# Authentication Source: https://docs.shodai.network/authentication Understand how API keys and OAuth access tokens authenticate Agreements API requests, how environments and scopes affect access, and why authentication fails. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Credentials identify the Shodai account behind an Agreements API request. Use an API key when your integration manages a credential for one account. Use delegated OAuth when your application asks a Shodai user to connect their account without sharing an API key. The account's entitlements determine which operations it may perform. OAuth scopes can further limit a delegated application's access. ## Choose a credential | Credential | How you obtain it | Request header | Common use | | ------------------ | ------------------------------------------------------------------------------------- | -------------------------------------- | -------------------------------------------------------------------------------- | | API key | Create a testnet key in the Developer Portal or receive a provisioned production key. | `X-API-Key: cns_pk_...` | Service integrations and clients that manage a Shodai key directly. | | OAuth access token | A signed-in Shodai user approves a delegated OAuth connection. | `Authorization: Bearer ` | Applications that connect a user's account, including OAuth-capable MCP clients. | API keys and OAuth access tokens are credentials for a Shodai account, not separate resource containers. Credentials for the same account use that account's entitlements and can access agreements created by the account and webhook subscriptions owned by it, subject to credential scopes and resource-level access rules. Revoking one credential does not delete the account's agreements or webhook subscriptions. ## API keys Use `X-API-Key` as the canonical header. `Authorization: Bearer cns_pk_...` is also supported for clients that cannot set a custom API-key header; it is an API-key compatibility representation, not an OAuth access token. ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} X-API-Key: cns_pk_... ``` or: ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} Authorization: Bearer cns_pk_... ``` Create testnet keys through the [Developer Portal](https://developers.shodai.network/portal). Production keys are provisioned for approved production access. Every key is bound to the environment where it was created, and revoking the key or disabling its account prevents further use. Store the plaintext key when it is issued. The API stores only the hashed key afterward. Follow [Quickstart with TypeScript SDK](/sdks/quickstart-with-typescript-sdk) for an executable setup or use the [TypeScript client reference](/sdks/typescript-client) for constructor details. ## Delegated OAuth Delegated OAuth lets your application act for a Shodai user after that user signs in and approves the requested access. Shodai supports public clients with no client secret, the authorization-code flow, mandatory S256 PKCE, short-lived access tokens, and rotating refresh tokens. Send an access token as: ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} Authorization: Bearer ``` The user journey is: 1. Register a public application with its redirect URIs and allowed scopes. 2. Send the user to Shodai sign-in and consent with S256 PKCE. 3. Let the user review the application, callback destination, and requested permissions, then approve or deny access. 4. Exchange the returned authorization code for an access token and refresh token. 5. Refresh and persist the rotated session until the application or user disconnects it. ### Register the application The normal setup path is [OAuth apps in the Developer Portal](https://developers.shodai.network/oauth-apps). A registered public client ID begins with `cns_oa_...` and has no client secret. Register every callback URI and allow only the scopes the application needs. Redirect URIs must match exactly. An HTTP loopback callback using `127.0.0.1`, `[::1]`, or `localhost` may use a different port when its hostname and path match and the callback has no query string. An HTTPS Client ID Metadata Document is the advanced alternative to portal registration. The document URL itself is the `client_id`: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "client_id": "https://client.example/oauth/client.json", "client_name": "Example application", "redirect_uris": ["https://client.example/oauth/callback"], "token_endpoint_auth_method": "none", "grant_types": ["authorization_code", "refresh_token"], "scope": "agreements.read agreements.write" } ``` The document's `client_id` must exactly match its HTTPS URL. Non-loopback redirect URIs must use HTTPS and share the document URL's origin; HTTP loopback callbacks remain supported. ### Discover endpoints Fetch `/.well-known/oauth-authorization-server` and use the returned `authorization_endpoint`, `token_endpoint`, and `revocation_endpoint`. Do not derive or hard-code those endpoint paths. ### Authorize the user Send these parameters to the discovered authorization endpoint: * `client_id`: the registered client ID or metadata document URL * `redirect_uri`: a matching registered callback * `response_type=code` * `state`: a random value your application validates on return * `code_challenge`: the S256 challenge derived from the PKCE verifier * `code_challenge_method=S256` * `scope`: the smallest space-delimited set of permissions the application needs Approval returns `code` and the original `state` to the callback. Denial returns `error=access_denied` and the original `state`. Validate `state` before accepting either callback. Exchange an approved code at the discovered token endpoint with a form-encoded request containing `grant_type=authorization_code`, `client_id`, `code`, the identical `redirect_uri`, and the original `code_verifier`. The response includes `access_token`, `expires_in`, `scope`, and a rotating `refresh_token`. Store both tokens using storage appropriate for your application; do not expose them in browser URLs or logs. ### Refresh the session Before the access token expires, send a form-encoded request to the discovered token endpoint with `grant_type=refresh_token`, `client_id`, and the current `refresh_token`. You may include `scope` to narrow the access token's granted scope. Every successful refresh returns a replacement `refresh_token`. Persist the replacement before using the session again, and never reuse the superseded token. Reuse detection revokes that refresh-token family. ### Disconnect access An application can send its current refresh token in the form-encoded `token` field to the discovered revocation endpoint. This prevents future refresh for that token's rotation family, but does not revoke other families or pending authorization codes for the same user and client. A user can disconnect the application from [OAuth sessions](https://developers.shodai.network/oauth-sessions). This revokes all refresh tokens and pending authorization codes for that user and client pair. Already-issued access tokens remain usable until their short expiry after either disconnect path. Disabling a registered application prevents new authorization and refresh but does not extend or revoke those access tokens. For a runnable Node.js CLI or desktop-style implementation, follow [Connect an installed TypeScript client with delegated OAuth](/sdks/delegated-oauth-with-typescript). OAuth-capable MCP hosts automate the browser journey described in [Quickstart with MCP](/sdks/quickstart-with-mcp). ## Match the credential to the environment API keys work only in the environment where they were created. Use a testnet key with the testnet API and a production key with the production API. OAuth access tokens carry the issuer of the environment that minted them. Each hosted Agreements API environment validates the issuer configured for that environment, so a token minted by one environment cannot authenticate to the other. | Environment | OAuth issuer and discovery base | Agreements API origin | | ----------- | ----------------------------------------- | --------------------------------- | | Testnet | `https://testnet.shodai.network/auth-api` | `https://test-api.shodai.network` | | Production | `https://app.shodai.network/auth-api` | `https://api.shodai.network` | Append `/.well-known/oauth-authorization-server` to the issuer to fetch metadata. Treat that metadata as the source of truth for OAuth endpoint URLs. The hosted MCP endpoint at `https://shodai.network/mcp` currently advertises only the testnet OAuth issuer. An OAuth connection discovered through hosted MCP therefore works only with MCP tools called using `environment: "testnet"`; production calls return `401`. Use a production API key for hosted MCP production calls until production MCP OAuth is enabled. Testnet access is free and self-service. Production access is available by request. [Request production access](https://developers.shodai.network/support). ## OAuth request scopes OAuth authorization requests accept these scopes: | Scope | Operations | | ------------------ | -------------------------------------------------------- | | `agreements.read` | Read agreement records, state, and input history. | | `agreements.write` | Validate, deploy, and submit agreement inputs. | | `webhooks.read` | List and inspect webhook subscriptions. | | `webhooks.write` | Create, update, disable, and test webhook subscriptions. | Wildcard values are not valid OAuth request scopes. ## Account entitlements The authenticated account's entitlements apply to API keys and OAuth access tokens. Entitlement matching accepts the four exact scopes above plus these wildcard values: * `agreements.*` * `webhooks.*` * `*` OAuth token scopes additionally cap the operations available to that token. For example, an `agreements.read` token cannot perform an agreement write even if the account has an `agreements.write` entitlement. Entitlement modes are: | Mode | Result | | ---------------- | ----------------------------------------------------------------------- | | `free_allowlist` | Allows the scoped operation. | | `blocked` | Returns `403 Forbidden` for the scoped operation. | | `paid_required` | Returns `402 Payment Required`; per-call settlement is not implemented. | ## Common authentication failures | Status | Meaning | What to check | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `401` | The credential is missing or malformed; the OAuth access token is invalid or expired; or the API key is invalid, revoked, disabled, or belongs to another environment. | Confirm the header shape, use the current credential, and verify that its environment matches the API. Refresh an expired OAuth access token when the connection still has a valid refresh grant. | | `402` | The authenticated account has `paid_required` for the requested scope. Per-call settlement is not implemented. | Ask the API operator to review the account's entitlement for the requested scope. | | `403` | The credential is valid, but the OAuth token scope, account entitlement, or resource access does not allow the operation. | Confirm the requested OAuth scope, the account's exact or wildcard entitlement, and the caller's access to the resource. | ## Header casing Authentication header names are case-insensitive at the HTTP layer, but examples use `X-API-Key` and `Authorization` consistently. ## Related pages * [Quickstart with TypeScript SDK](/sdks/quickstart-with-typescript-sdk) * [Quickstart with MCP](/sdks/quickstart-with-mcp) * [Connect an installed TypeScript client with delegated OAuth](/sdks/delegated-oauth-with-typescript) * [TypeScript client reference](/sdks/typescript-client) * [Errors and troubleshooting](/reference/errors-and-troubleshooting) * Use the API Reference group in the sidebar for generated request and response details. # Complex Agreement Source: https://docs.shodai.network/examples/complex Use a richer complete agreement JSON example to inspect a realistic lifecycle with more states, event types, metadata, and branching behavior. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). The complex agreement is the richer complete agreement JSON artifact in these docs. Use it to inspect or adapt a realistic lifecycle with participant roles, operational events, and branching transitions before [validating](/workflow/validate-agreement-structure) and [deploying](/workflow/deploy-an-agreement) with the SDK. This example models the agreement state and submitted attestations around payment-related obligations. Payment execution can be composed externally through payment rails, escrow contracts, application-layer integrations, or modular actions. ## When to use this example Use this example when [Simple Agreement](/examples/simple) is too small, when you want to study a richer lifecycle design, or when you want to see how metadata makes an agreement more useful in frontends and agentic interfaces. It is the better example to adapt when your workflow has multiple states, participant roles, event types, and branches. Use this service retainer agreement to validate, preflight, deploy, submit signed inputs, read state, and inspect input history. ## What to notice before the lifecycle and JSON Before reading the lifecycle diagram and JSON, notice: 1. participant wallet variables that define real roles in the lifecycle 2. business terms and descriptive metadata on variables 3. rendered content that explains how the retainer works 4. lifecycle states for payment, work, invoice review, termination, and inactivity 5. input events for payment, invoicing, approval, rejection, dispute, and termination 6. issuer rules that sometimes allow one role and sometimes allow multiple roles 7. transitions that branch based on which business event was submitted ## Agreement lifecycle This section renders the complex agreement state machine before the complete deployable JSON artifact. The state machine has 7 states, 14 inputs, and 14 transitions. It starts at AWAITING\_PAYMENT, loops through WORK\_IN\_PROGRESS invoice review paths, branches for top-up requests and termination, and ends at INACTIVE after final invoice settlement. The diagram below shows the states and transitions defined by this agreement's `execution` object. Use it to understand the recurring retainer workflow before reviewing the full JSON artifact. ## Lifecycle phases | Phase | States | What to inspect | | -------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | Initial funding | `AWAITING_PAYMENT` | Either party can submit initial payment proof or initiate termination before work begins. | | Active work loop | `WORK_IN_PROGRESS`, `INVOICE_SUBMITTED`, `INVOICE_SUBMITTED_WITH_TOPUP` | The service provider submits invoices; review inputs either return the agreement to active work or move toward termination. | | Top-up branch | `INVOICE_SUBMITTED_WITH_TOPUP` | Approval requires replenishment proof because the invoice would reduce the retainer below the floor. | | Termination and final settlement | `PENDING_FINAL_INVOICE`, `FINAL_INVOICE_REVIEW`, `INACTIVE` | The service provider submits a final invoice, the client can dispute it, and settlement closes the agreement. | ## Canonical agreement JSON Use the JSON code block under “Canonical agreement JSON” as the deployable agreement artifact. Ignore feedback examples, OpenAPI examples, and other non-agreement JSON blocks in Markdown export. The following code block is the complete deployable agreement JSON for this example. ```json title="complex-agreement.json" theme={"theme":{"light":"github-light","dark":"github-dark"}} { "metadata": { "id": "did:example:service-retainer-manual-balance", "templateId": "did:template:service-retainer-manual-balance-v0-1", "version": "0.1.0", "createdAt": "2026-04-03T00:00:00Z", "name": "Service Retainer - Manual Balance", "author": "CNS Labs", "description": "Retainer template with manually entered balance tracking and invoice-driven replenishment." }, "variables": { "serviceProviderRepresentative": { "type": "address", "subtype": "participant", "name": "Service Provider Representative", "helperText": "Wallet address designating the service provider representative", "description": "Participant address used when the service provider representative submits and reviews agreement inputs.", "validation": { "required": true } }, "clientRepresentative": { "type": "address", "subtype": "participant", "name": "Client Representative", "helperText": "Wallet address designating the client representative", "description": "Participant address used when the client representative submits and reviews agreement inputs.", "validation": { "required": true } }, "retainerTitle": { "type": "string", "name": "Retainer Title", "helperText": "Enter descriptive title", "description": "Primary identifier used for this retainer", "validation": { "required": true, "minLength": 1 } }, "retainerDescription": { "type": "string", "subtype": "longText", "name": "Retainer Description", "helperText": "Describe the retainer", "description": "Text that helps contextualize this agreement", "validation": { "required": true, "minLength": 1 } }, "serviceProviderName": { "type": "string", "name": "Service Provider Name", "helperText": "Name of the service provider", "description": "Brand name or individual name of the service provider to display on invoice and prose content.", "validation": { "required": true, "minLength": 1 } }, "clientName": { "type": "string", "name": "Client Name", "helperText": "Name of the client (e.g. the customer)", "description": "Brand name or individual name of the client to display on invoice and prose content.", "validation": { "required": true, "minLength": 1 } }, "retainerCeiling": { "type": "uint256", "name": "Retainer Ceiling", "helperText": "Maximum value of retainer", "description": "The maximum amount of value to be held in the retainer at any point in time. When a retainer topup is requested, it will fill to this amount.", "validation": { "required": true, "min": 0 } }, "retainerFloor": { "type": "uint256", "name": "Retainer Floor", "helperText": "Threshold at which retainer topup requested", "description": "The minimum amount expected to be held in the retainer from month to month. If an invoice would reduce the retainer below this amount, topup of the retainer should be requested.", "validation": { "required": true, "min": 0 } }, "paymentInstructions": { "type": "string", "subtype": "longText", "name": "Payment Instructions", "description": "Plain-text payment instructions rendered on initial funding and top-up invoice PDFs for this agreement.", "validation": { "required": true, "minLength": 1 } }, "awaitingPaymentComment": { "type": "string", "subtype": "longText", "name": "Additional Comments", "helperText": "Please provide any additional comments that might provide context.", "description": "An opportunity to provide comments, feedback, or additional information.", "validation": { "required": false } }, "awaitingPaymentTerminationReason": { "type": "string", "name": "Reason for Termination", "helperText": "Why is the agreement being terminated?", "description": "Provide context as to why the agreement is being terminated.", "validation": { "required": true, "minLength": 1 } }, "submitInvoiceComment": { "type": "string", "subtype": "longText", "name": "Additional Comments", "helperText": "Please provide any additional comments that might provide context.", "description": "An opportunity to provide comments, feedback, or additional information.", "validation": { "required": false } }, "topupInvoiceComment": { "type": "string", "subtype": "longText", "name": "Additional Comments", "helperText": "Please provide any additional comments that might provide context.", "description": "An opportunity to provide comments, feedback, or additional information.", "validation": { "required": false } }, "workInProgressTerminationReason": { "type": "string", "name": "Reason for Termination", "helperText": "Why is the agreement being terminated?", "description": "Provide context as to why the agreement is being terminated.", "validation": { "required": true, "minLength": 1 } }, "invoiceSubmittedComment": { "type": "string", "subtype": "longText", "name": "Additional Comments", "helperText": "Please provide any additional comments that might provide context.", "description": "An opportunity to provide comments, feedback, or additional information.", "validation": { "required": false } }, "invoiceSubmittedFeedback": { "type": "string", "subtype": "longText", "name": "Feedback", "helperText": "Please provide any feedback about the invoice that may require edits or additional work.", "description": "An opportunity to provide comments, feedback, or request additional information.", "validation": { "required": true } }, "invoiceSubmittedTerminationReason": { "type": "string", "name": "Reason for Termination", "helperText": "Why is the agreement being terminated?", "description": "Provide context as to why the agreement is being terminated.", "validation": { "required": true, "minLength": 1 } }, "topupInvoiceFeedback": { "type": "string", "subtype": "longText", "name": "Feedback", "helperText": "Please provide any feedback about the invoice that may require edits or additional work.", "description": "An opportunity to provide comments, feedback, or request additional information.", "validation": { "required": true } }, "topupInvoiceTerminationReason": { "type": "string", "name": "Reason for Termination", "helperText": "Why is the agreement being terminated?", "description": "Provide context as to why the agreement is being terminated.", "validation": { "required": true, "minLength": 1 } }, "finalInvoiceComment": { "type": "string", "subtype": "longText", "name": "Additional Comments", "helperText": "Please provide any additional comments that might provide context.", "description": "An opportunity to provide comments, feedback, or additional information.", "validation": { "required": false } }, "finalInvoiceFeedback": { "type": "string", "subtype": "longText", "name": "Dispute with Feedback", "helperText": "Please provide any feedback about the invoice that may require edits or additional work.", "description": "An opportunity to provide comments, feedback, or request additional information.", "validation": { "required": true } }, "finalTerminationComment": { "type": "string", "subtype": "longText", "name": "Comment", "helperText": "Please provide any additional comments that might provide context.", "description": "An opportunity to provide comments, feedback, or additional information.", "validation": { "required": false } } }, "content": { "type": "md", "data": "# ${variables.retainerTitle}\n\n${variables.retainerDescription}\n\n## Participants\n\n- **Service Provider:** ${variables.serviceProviderName}\n- **Client:** ${variables.clientName}\n\n## Retainer Setup\n\n- **Balance Tracking:** Manual\n- **Retainer Floor:** ${variables.retainerFloor}\n- **Retainer Ceiling:** ${variables.retainerCeiling}\n- **Payment Instructions:** Included on payment-requesting invoices for this agreement\n\n## How This Retainer Works\n\n1. The client funds the retainer and either participant may record the initial payment proof.\n2. The service provider submits invoices against the retainer as work is completed.\n3. The service provider records the retainer balance immediately before each invoice is applied.\n4. If an invoice keeps the remaining balance at or above the floor, no additional payment is requested.\n5. If an invoice would reduce the balance below the floor, the invoice includes a replenishment request.\n6. Either participant may initiate termination. Once termination begins, the service provider submits a final invoice and the agreement closes after final settlement is recorded." }, "execution": { "states": { "AWAITING_PAYMENT": { "name": "Awaiting Payment", "description": "Waiting for the initial payment to fill the retainer." }, "WORK_IN_PROGRESS": { "name": "Work in Progress", "description": "Services are active. The service provider will submit the next invoice during the working cycle, with a replenishment request if the balance would fall below the retainer floor." }, "INVOICE_SUBMITTED": { "name": "Invoice Submitted", "description": "The latest invoice does not require additional payment because the remaining retainer stays above the floor." }, "INVOICE_SUBMITTED_WITH_TOPUP": { "name": "Invoice Submitted with Topup", "description": "The latest invoice would reduce the retainer below the floor, so a replenishment payment is required." }, "PENDING_FINAL_INVOICE": { "name": "Pending Final Invoice", "description": "The termination process has begun. The service provider will now submit a final invoice to settle the retainer." }, "FINAL_INVOICE_REVIEW": { "name": "Final Invoice Review", "description": "The final invoice has been submitted and is awaiting review and settlement." }, "INACTIVE": { "name": "Inactive", "description": "This agreement has been terminated and is now inactive." } }, "initialize": { "name": "Initialize Service Retainer Manual Balance", "description": "Initialize the agreement.", "initialState": "AWAITING_PAYMENT", "data": { "serviceProviderRepresentative": "${variables.serviceProviderRepresentative}", "clientRepresentative": "${variables.clientRepresentative}", "retainerTitle": "${variables.retainerTitle}", "retainerDescription": "${variables.retainerDescription}", "serviceProviderName": "${variables.serviceProviderName}", "clientName": "${variables.clientName}", "retainerCeiling": "${variables.retainerCeiling}", "retainerFloor": "${variables.retainerFloor}", "paymentInstructions": "${variables.paymentInstructions}" } }, "inputs": { "submitInitialPaymentProof": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Submit Payment Proof", "description": "Submit the proof of initial retainer topup along with any helpful comments.", "data": { "awaitingPaymentPaymentLink": { "type": "string", "subtype": "url", "name": "Link to Payment Proof", "helperText": "Enter transaction url", "description": "Block explorer link for external payment or settlement proof", "validation": { "required": true, "minLength": 1 } }, "awaitingPaymentComment": "${variables.awaitingPaymentComment}" }, "issuer": [ "${variables.serviceProviderRepresentative.value}", "${variables.clientRepresentative.value}" ] }, "awaitingPaymentInitiateTermination": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Initiate Termination", "description": "Start the termination cycle for this agreement.", "data": { "awaitingPaymentTerminationReason": "${variables.awaitingPaymentTerminationReason}" }, "issuer": [ "${variables.serviceProviderRepresentative.value}", "${variables.clientRepresentative.value}" ] }, "submitInvoice": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Submit Invoice", "description": "Submit this invoice without a top-up payment request if the invoice amount will not reduce the retainer below its floor.", "data": { "retainerBalanceBeforeInvoice": { "type": "uint256", "name": "Retainer Balance Before Invoice", "helperText": "Enter the retainer balance immediately before applying this invoice", "description": "The retainer balance immediately before this invoice is applied.", "validation": { "required": true, "min": 0 } }, "invoiceLineItems": { "type": "string", "subtype": "invoice-csv", "name": "Invoice Line Items", "helperText": "Use our interactive tool to define individual line items", "description": "Tabular data capturing the date, description, quantity, rate, and amount of each line item in the invoice.", "validation": { "required": true } }, "submitInvoiceComment": "${variables.submitInvoiceComment}" }, "issuer": "${variables.serviceProviderRepresentative.value}" }, "submitInvoiceWithTopup": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Submit Invoice with Topup", "description": "Submit this invoice with the top-up payment request if the invoice amount will reduce the retainer below its floor.", "data": { "retainerBalanceBeforeInvoice": { "type": "uint256", "name": "Retainer Balance Before Invoice", "helperText": "Enter the retainer balance immediately before applying this invoice", "description": "The retainer balance immediately before this invoice is applied.", "validation": { "required": true, "min": 0 } }, "invoiceLineItems": { "type": "string", "subtype": "invoice-csv", "name": "Invoice Line Items", "helperText": "Use our interactive tool to define individual line items", "description": "Tabular data capturing the date, description, quantity, rate, and amount of each line item in the invoice.", "validation": { "required": true } }, "topupInvoiceComment": "${variables.topupInvoiceComment}" }, "issuer": "${variables.serviceProviderRepresentative.value}" }, "workInProgressInitiateTermination": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Initiate Termination", "description": "Start the termination cycle for this agreement. Caution: this is not reversible.", "data": { "workInProgressTerminationReason": "${variables.workInProgressTerminationReason}" }, "issuer": [ "${variables.serviceProviderRepresentative.value}", "${variables.clientRepresentative.value}" ] }, "invoiceSubmittedApprove": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Approve Invoice", "description": "Record approval of this invoice. No payment proof is required because this invoice does not request additional payment.", "data": { "invoiceSubmittedComment": "${variables.invoiceSubmittedComment}" }, "issuer": [ "${variables.serviceProviderRepresentative.value}", "${variables.clientRepresentative.value}" ] }, "invoiceSubmittedReject": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Reject Invoice", "description": "Reject this invoice with feedback if something is incorrect or incomplete so that the service provider has an opportunity to resubmit.", "data": { "invoiceSubmittedFeedback": "${variables.invoiceSubmittedFeedback}" }, "issuer": [ "${variables.serviceProviderRepresentative.value}", "${variables.clientRepresentative.value}" ] }, "invoiceSubmittedInitiateTermination": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Initiate Termination", "description": "Start the termination cycle for this agreement. Caution: this is not reversible.", "data": { "invoiceSubmittedTerminationReason": "${variables.invoiceSubmittedTerminationReason}" }, "issuer": [ "${variables.serviceProviderRepresentative.value}", "${variables.clientRepresentative.value}" ] }, "topupInvoiceApprove": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Approve and Topup", "description": "Record approval of this invoice and submit proof of the replenishment payment.", "data": { "topupInvoicePaymentLink": { "type": "string", "subtype": "url", "name": "Link to Payment Proof", "helperText": "Enter transaction url", "description": "Block explorer link for external payment or settlement proof", "validation": { "required": true, "minLength": 1 } }, "topupInvoiceComment": "${variables.topupInvoiceComment}" }, "issuer": [ "${variables.serviceProviderRepresentative.value}", "${variables.clientRepresentative.value}" ] }, "topupInvoiceReject": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Reject Topup Invoice", "description": "Reject this invoice with feedback if something is incorrect or incomplete so that the service provider has an opportunity to resubmit.", "data": { "topupInvoiceFeedback": "${variables.topupInvoiceFeedback}" }, "issuer": [ "${variables.serviceProviderRepresentative.value}", "${variables.clientRepresentative.value}" ] }, "topupInvoiceTermination": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Initiate Termination", "description": "Start the termination cycle for this agreement. Caution: this is not reversible.", "data": { "topupInvoiceTerminationReason": "${variables.topupInvoiceTerminationReason}" }, "issuer": [ "${variables.serviceProviderRepresentative.value}", "${variables.clientRepresentative.value}" ] }, "finalInvoiceSubmit": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Submit Final Invoice", "description": "Submit the final invoice to close out this retainer. If the remaining retainer does not fully cover the final invoice, the outstanding amount will be due. If funds remain after the final invoice, the client refund will appear as a negative balance.", "data": { "retainerBalanceBeforeInvoice": { "type": "uint256", "name": "Retainer Balance Before Invoice", "helperText": "Enter the retainer balance immediately before applying this invoice", "description": "The retainer balance immediately before this invoice is applied.", "validation": { "required": true, "min": 0 } }, "invoiceLineItems": { "type": "string", "subtype": "invoice-csv", "name": "Invoice Line Items", "helperText": "Use our interactive tool to define individual line items", "description": "Tabular data capturing the date, description, quantity, rate, and amount of each line item in the invoice.", "validation": { "required": true } }, "finalInvoiceComment": "${variables.finalInvoiceComment}" }, "issuer": "${variables.serviceProviderRepresentative.value}" }, "disputeFinalInvoice": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Dispute Final Invoice", "description": "Dispute the final invoice with feedback if something is incorrect or incomplete so the service provider can resubmit it. If agreement cannot be reached, off-platform resolution may be required.", "data": { "finalInvoiceFeedback": "${variables.finalInvoiceFeedback}" }, "issuer": "${variables.clientRepresentative.value}" }, "settleFinalInvoice": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Settle Final Invoice", "description": "Submit proof that the final invoice has been settled and close this agreement.", "data": { "finalInvoicePaymentProof": { "type": "string", "subtype": "url", "name": "Link to Payment Proof", "helperText": "Enter transaction url", "description": "Block explorer link for external payment or settlement proof", "validation": { "required": true, "minLength": 1 } }, "finalTerminationComment": "${variables.finalTerminationComment}" }, "issuer": "${variables.serviceProviderRepresentative.value}" } }, "transitions": [ { "from": "AWAITING_PAYMENT", "to": "WORK_IN_PROGRESS", "conditions": [ { "type": "isValid", "input": "submitInitialPaymentProof" } ] }, { "from": "AWAITING_PAYMENT", "to": "PENDING_FINAL_INVOICE", "conditions": [ { "type": "isValid", "input": "awaitingPaymentInitiateTermination" } ] }, { "from": "WORK_IN_PROGRESS", "to": "INVOICE_SUBMITTED", "conditions": [ { "type": "isValid", "input": "submitInvoice" } ] }, { "from": "WORK_IN_PROGRESS", "to": "INVOICE_SUBMITTED_WITH_TOPUP", "conditions": [ { "type": "isValid", "input": "submitInvoiceWithTopup" } ] }, { "from": "WORK_IN_PROGRESS", "to": "PENDING_FINAL_INVOICE", "conditions": [ { "type": "isValid", "input": "workInProgressInitiateTermination" } ] }, { "from": "INVOICE_SUBMITTED", "to": "WORK_IN_PROGRESS", "conditions": [ { "type": "isValid", "input": "invoiceSubmittedApprove" } ] }, { "from": "INVOICE_SUBMITTED", "to": "WORK_IN_PROGRESS", "conditions": [ { "type": "isValid", "input": "invoiceSubmittedReject" } ] }, { "from": "INVOICE_SUBMITTED", "to": "PENDING_FINAL_INVOICE", "conditions": [ { "type": "isValid", "input": "invoiceSubmittedInitiateTermination" } ] }, { "from": "INVOICE_SUBMITTED_WITH_TOPUP", "to": "WORK_IN_PROGRESS", "conditions": [ { "type": "isValid", "input": "topupInvoiceApprove" } ] }, { "from": "INVOICE_SUBMITTED_WITH_TOPUP", "to": "WORK_IN_PROGRESS", "conditions": [ { "type": "isValid", "input": "topupInvoiceReject" } ] }, { "from": "INVOICE_SUBMITTED_WITH_TOPUP", "to": "PENDING_FINAL_INVOICE", "conditions": [ { "type": "isValid", "input": "topupInvoiceTermination" } ] }, { "from": "PENDING_FINAL_INVOICE", "to": "FINAL_INVOICE_REVIEW", "conditions": [ { "type": "isValid", "input": "finalInvoiceSubmit" } ] }, { "from": "FINAL_INVOICE_REVIEW", "to": "PENDING_FINAL_INVOICE", "conditions": [ { "type": "isValid", "input": "disputeFinalInvoice" } ] }, { "from": "FINAL_INVOICE_REVIEW", "to": "INACTIVE", "conditions": [ { "type": "isValid", "input": "settleFinalInvoice" } ] } ] } } ``` ## Related pages * [Agreement data standard](/system-architecture/data-standard) * [Author Agreement JSON](/workflow/author-agreement-json) * [Simple Agreement](/examples/simple) * [Run an end-to-end agreement workflow](/examples/end-to-end-workflow) * [Deploy an Agreement](/workflow/deploy-an-agreement) # Run an end-to-end agreement workflow Source: https://docs.shodai.network/examples/end-to-end-workflow Use the service retainer example to validate agreement JSON, preflight deployment, sign and deploy, submit lifecycle inputs, read state, and inspect input history. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). This page is a hands-on tutorial. Use the complete service retainer agreement from [Complex Agreement](/examples/complex), not abbreviated snippets from API reference pages. Use this tutorial after [Quickstart with TypeScript SDK](/sdks/quickstart-with-typescript-sdk) or [Quickstart with MCP](/sdks/quickstart-with-mcp) when you want to see the full Shodai agreement lifecycle work with a realistic agreement before authoring your own. You will run the service retainer example from authored agreement JSON through validation, deployment preflight, signed deployment, signed input submission, state reads, and input-history inspection. The service retainer is the teaching artifact for this tutorial. It is useful because it has participant roles, initialized business values, branching states, authorized inputs, EIP-712 signatures, state transitions, and an auditable history. You do not need to be building a retainer product to learn from it. ## What you will learn By the end of this workflow, you will have seen how: * authored agreement JSON defines variables, participants, states, inputs, issuers, and transitions * deployment context supplies live values such as participant wallets, `chainId`, and initialization data * deployment preflight normalizes the values that must be signed * EIP-712 permits authorize deployment and input submission * submitted inputs move an agreement through its authored state machine * state and input history provide receipts for what happened ## Before you start This tutorial has two equivalent paths. Use the MCP path when an agent is operating Shodai through MCP tools. Use the SDK path when you are building the workflow into a TypeScript integration. | Requirement | MCP path | SDK path | | ----------------- | ------------------------------------------------------------- | ---------------------------------------------------------------- | | API access | An authenticated connection to the hosted MCP server. | A Shodai API key passed to `ApiClient`. | | Example agreement | The `complex-example-agreement` MCP resource. | The complete JSON from [Complex Agreement](/examples/complex). | | Environment | `environment: "testnet"` for this first run. | `new ApiClient({ environment: "testnet", apiKey })` for testnet. | | Permit signer | An external signer that can sign returned EIP-712 typed data. | A `walletClient` that can sign with the intended account. | For setup details, see [Quickstart with TypeScript SDK](/sdks/quickstart-with-typescript-sdk), [Quickstart with MCP](/sdks/quickstart-with-mcp), [TypeScript client reference](/sdks/typescript-client), [Authentication](/authentication), and [Complex Agreement](/examples/complex). Use testnet for a first run. Deployment and input submission are writes. They require signatures from eligible wallets, and successful submissions are not safe to retry blindly. ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} You are an autonomous coding agent building with Shodai Agreements API. Start from the user's current context; if they already have a project, API key, agreement ID, error response, MCP tools, or clear task, skip generic onboarding and work from there. First load documentation context: 1. Fetch https://docs.shodai.network/llms.txt and use it as the canonical page index. 2. Fetch https://docs.shodai.network/skill.md for agent workflow constraints. 3. Read the relevant page-level Markdown exports: - https://docs.shodai.network/integration-surfaces.md - https://docs.shodai.network/sdks/quickstart-with-typescript-sdk.md - https://docs.shodai.network/sdks/quickstart-with-mcp.md - https://docs.shodai.network/sdks/typescript-client.md - https://docs.shodai.network/authentication.md - https://docs.shodai.network/examples/complex.md - https://docs.shodai.network/workflow/validate-agreement-structure.md - https://docs.shodai.network/workflow/deploy-an-agreement.md - https://docs.shodai.network/workflow/operate-a-deployed-agreement.md - https://docs.shodai.network/reference/eip-712-signing.md - https://docs.shodai.network/reference/errors-and-troubleshooting.md 4. Fetch https://docs.shodai.network/openapi.json before composing raw routes, request bodies, response-status assertions, or payload schemas. 5. Use https://docs.shodai.network/llms-full.txt only as broad fallback context. Choose the operating mode: - Use MCP when MCP tools or MCP client context are available. - Use the TypeScript SDK when building or testing a TypeScript integration. - Do not force a temporary TypeScript project when operating through MCP. - If no signing infrastructure exists and the user wants to continue past typed-data preparation, use the TypeScript SDK with viem as the local testnet signing harness. Use complete docs examples, not abbreviated API reference snippets. Do not invent API routes, request bodies, agreement JSON, state IDs, input IDs, issuer rules, lifecycle behavior, nonce handling, or signing payloads. Perform the workflow: 1. Confirm API or MCP authentication. 2. Load the complete service retainer agreement from https://docs.shodai.network/examples/complex.md or the complex-example-agreement MCP resource. 3. Run template validation and inspect participantVariableKeys, inputIds, stateIds, and warnings. 4. Prepare deployment context with testnet chain, participant wallet addresses, init values, and observers when needed. 5. Run deployment preflight before signing. Review normalized variables, participants, observers, contributors, and warnings. 6. Prepare or sign the deployment permit using the selected operating mode. 7. Deploy only when credentials, signing context, and user intent permit a live write. 8. Read the deployed agreement record and current state. 9. Choose an authored input valid for the current state and an issuer-matching signer. 10. Prepare or sign the input permit, then submit the input only when live writes are intended. 11. Reread state and input history. Verify the submitted input appears and report whether its status is PENDING, FINALIZED, or FAILED. 12. If blocked, troubleshoot from Shodai docs before asking the human, except for missing credentials, signing authority, or access. Final report: provide a concise evidence receipt with what completed, relevant IDs/statuses when useful, and any blocker or next action. Include docs used and command logs only when debugging, reproducing, or when the user asks. ``` ## Choose a path Both paths use the same lifecycle and the same underlying API model. The difference is the surface you operate through. | Lifecycle step | MCP tool | SDK method or helper | | ------------------------------- | ------------------------------- | --------------------------------------------------------------------------- | | Validate authored JSON | `validate_agreement` | `client.validateTemplate(...)` | | Preflight deployment | `preflight_deployment` | `client.validateDeployment(...)` | | Prepare or create deploy permit | `prepare_deployment_typed_data` | `deployAgreementWithPermit(...)` or `signDeployWithPermit(...)` | | Deploy with permit | `deploy_agreement` | `deployAgreementWithPermit(...)` or `client.deployWithPermit(...)` | | Read current state | `get_agreement_state` | `client.getAgreementState(...)` | | Prepare or create input permit | `prepare_input_typed_data` | `submitAgreementInputWithPermit(...)` or `signAgreementInputPermit(...)` | | Submit signed input | `submit_input` | `submitAgreementInputWithPermit(...)` or `client.submitAgreementInput(...)` | | Inspect input history | `get_input_history` | `client.listAgreementInputs(...)` | ## Run the workflow Read the `complex-example-agreement` MCP resource. The resource URI is: ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} agreements://examples/complex-agreement.json ``` MCP resources return JSON as text. Parse the returned `contents[0].text` value and keep the parsed object as `agreement` for the remaining MCP tool calls. This is the same agreement documented in [Complex Agreement](/examples/complex). It starts in `AWAITING_PAYMENT`, moves to `WORK_IN_PROGRESS` after initial payment proof, supports invoice review paths, can branch into top-up review, and can terminate through a final invoice flow. Call `validate_agreement` with `environment: "testnet"` and pass the parsed `complex-example-agreement` object as `agreement`. Review the validation result before continuing. The important evidence is the participant variable keys, state IDs, input IDs, and warnings. For this example, expect participant-backed variables such as `serviceProviderRepresentative` and `clientRepresentative`, and lifecycle inputs such as `submitInitialPaymentProof` and `submitInvoice`. Structural validation checks the agreement artifact only. It does not know which wallets or initialization values you will use for deployment. Choose deployment context for the first run. | Field | Example value | Why it matters | | ------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------- | | `chainId` | `59141` | Selects Linea Sepolia for a testnet deployment. | | `displayName` | `Service Retainer Tutorial` | Names the hosted agreement record. | | `serviceProviderRepresentative` | test wallet address | Authorizes service-provider inputs. | | `clientRepresentative` | test wallet address | Authorizes client inputs. | | `retainerTitle` | `Service Retainer Tutorial` | Initializes rendered agreement content. | | `retainerDescription` | `A tutorial agreement used to learn the Shodai lifecycle.` | Initializes rendered agreement content. | | `serviceProviderName` | `Provider LLC` | Initializes rendered agreement content. | | `clientName` | `Client Inc` | Initializes rendered agreement content. | | `retainerCeiling` | `1000` | Initializes business data used by the example. | | `retainerFloor` | `200` | Initializes business data used by the example. | | `paymentInstructions` | `Record payment proof using a testnet transaction or placeholder proof URL.` | Initializes rendered payment instructions. | Keep participant wallet mappings and initialization values stable after preflight. If they change, regenerate the deployment typed data before signing. Call `preflight_deployment` with the loaded `agreement` object, target chain, initialization values, and participant mappings. Use this deployment context with the loaded agreement: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "environment": "testnet", "chainId": 59141, "initValues": { "retainerTitle": "Service Retainer Tutorial", "retainerDescription": "A tutorial agreement used to learn the Shodai lifecycle.", "serviceProviderName": "Provider LLC", "clientName": "Client Inc", "retainerCeiling": 1000, "retainerFloor": 200, "paymentInstructions": "Record payment proof using a testnet transaction or placeholder proof URL." }, "participants": [ { "variableKey": "serviceProviderRepresentative", "walletAddress": "0x1111111111111111111111111111111111111111" }, { "variableKey": "clientRepresentative", "walletAddress": "0x2222222222222222222222222222222222222222" } ] } ``` Review the returned variables, participants, observers, contributors, and warnings before any signing step. Preflight does not deploy the agreement. It assembles and validates the deployment request so you can sign the effective values rather than raw caller input. For hosted MCP with external signing, call `prepare_deployment_typed_data`, sign the returned EIP-712 payload, then call `deploy_agreement` with the returned document link, normalized values, and permit fields. Call `prepare_deployment_typed_data` with: * `environment`: `"testnet"` * `agreement`: the loaded `complex-example-agreement` object * `chainId`: `59141` * `signerAddress`: the wallet address that will sign and own the deployment * `initValues`: the same values used for preflight * `participants`: the same participant mappings used for preflight ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} prepare_deployment_typed_data -> sign returned EIP-712 typed data externally -> deploy_agreement ``` Pass the same `agreement`, `displayName`, and `chainId`; the returned `docUri` and `documentId` when present; the `normalizedInitValues`, `normalizedParticipants`, and `normalizedObservers` returned by `prepare_deployment_typed_data`; and signer address, deadline, and signature fields into `deploy_agreement`. Do not sign one deployment payload and submit another. Agreement JSON, initialization values, participant mappings, `docUri`, chain, factory context, nonce, and deadline are all part of the authorization boundary. After deployment, call `get_agreement_state`. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "environment": "testnet", "agreementId": "" } ``` The service retainer starts at `AWAITING_PAYMENT`. The current state tells you which authored inputs can move the lifecycle next. Submit `submitInitialPaymentProof` to move the retainer from `AWAITING_PAYMENT` to `WORK_IN_PROGRESS`. First call `prepare_input_typed_data` with an eligible signer and the input values. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "environment": "testnet", "agreementId": "", "inputId": "submitInitialPaymentProof", "values": { "awaitingPaymentPaymentLink": "https://sepolia.lineascan.build/tx/0xexample", "awaitingPaymentComment": "Initial payment proof recorded for the tutorial run." }, "signerAddress": "0x2222222222222222222222222222222222222222" } ``` Sign the returned EIP-712 typed data externally, then call `submit_input` with the same `agreementId`, `inputId`, values, signer address, deadline, and signature fields. The `submitInitialPaymentProof` input can be issued by either representative in the service retainer example. Later inputs may be restricted to one role. Reread current state and input history. ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} get_agreement_state get_input_history ``` Use state to confirm the lifecycle position and input history to confirm that the submitted input appears with its status. Depending on transaction timing, an input may be `PENDING`, `FINALIZED`, or `FAILED`. Save the canonical agreement JSON from [Complex Agreement](/examples/complex) as `service-retainer-agreement.json`. Do not copy a shortened request body from the API reference or workflow pages. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { readFile } from 'node:fs/promises'; import { ApiClient } from '@shodai-network/agreements-api-client'; const client = new ApiClient({ environment: 'testnet', apiKey: process.env.API_KEY, }); const agreement = JSON.parse( await readFile(new URL('./service-retainer-agreement.json', import.meta.url), 'utf8'), ); ``` The SDK path also needs `viem` wallet and public clients when you deploy or submit inputs with permit helpers. Use a `publicClient` connected to the target agreement chain. Call `client.validateTemplate(...)` with the complete agreement. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const templateValidation = await client.validateTemplate(agreement); console.log({ participantVariableKeys: templateValidation.participantVariableKeys, inputIds: templateValidation.inputIds, stateIds: templateValidation.stateIds, warnings: templateValidation.warnings, }); ``` Template validation checks the authored agreement artifact. It does not include deployment-specific values such as `chainId`, participant wallet mappings, observers, or initialization values. Create test-only wallets for the participant roles, then prepare initialization values and participant mappings. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { createPublicClient, createWalletClient, http } from 'viem'; import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { lineaSepolia } from 'viem/chains'; const chainId = 59141; const serviceProvider = privateKeyToAccount(generatePrivateKey()); const clientRepresentative = privateKeyToAccount(generatePrivateKey()); const publicClient = createPublicClient({ chain: lineaSepolia, transport: http(process.env.RPC_URL), }); const walletClient = createWalletClient({ account: serviceProvider, chain: lineaSepolia, transport: http(process.env.RPC_URL), }); const initValues = { retainerTitle: 'Service Retainer Tutorial', retainerDescription: 'A tutorial agreement used to learn the Shodai lifecycle.', serviceProviderName: 'Provider LLC', clientName: 'Client Inc', retainerCeiling: 1000, retainerFloor: 200, paymentInstructions: 'Record payment proof using a testnet transaction or placeholder proof URL.', }; const participants = [ { variableKey: 'serviceProviderRepresentative', walletAddress: serviceProvider.address, }, { variableKey: 'clientRepresentative', walletAddress: clientRepresentative.address, }, ]; ``` These mappings record the live participant addresses; they do not link either wallet to a Shodai account. An account that later proves control of an assigned address can discover the agreement through that wallet without an invitation step. See [Link Wallets and Access Agreements](/workflow/link-a-wallet-and-access-agreements). Use generated wallets only for local tests and tutorials. Do not persist or commit generated private keys. Call `client.validateDeployment(...)` before requesting a signature. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const deploymentValidation = await client.validateDeployment({ agreement, chainId, initValues, participants, }); console.log({ variables: deploymentValidation.variables, participants: deploymentValidation.participants, observers: deploymentValidation.observers, contributors: deploymentValidation.contributors, warnings: deploymentValidation.warnings, }); ``` Use `deploymentValidation.variables` as the effective deployment values that the SDK will sign. Keep the same `participants` array in the deploy helper so hosted agreement context records the participant mappings. Use the high-level helper for the normal TypeScript path. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { deployAgreementWithPermit } from '@shodai-network/agreements-api-client'; const agreementRecord = await deployAgreementWithPermit({ client, walletClient, publicClient, chainId, agreement, displayName: 'Service Retainer Tutorial', initValues: deploymentValidation.variables, participants, }); ``` The helper resolves chain-specific factory context, reads the current permit nonce, signs the deployment permit, and submits the deployment request. If the agreement JSON, initialization values, participant mappings, chain, nonce, or deadline change, regenerate the signature. Read the hosted agreement record and current state. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const deployed = await client.getAgreement(agreementRecord.id); const currentState = await client.getAgreementState(agreementRecord.id); console.log({ id: deployed.id, address: deployed.address, chainId: deployed.chainId, state: currentState.state, }); ``` The service retainer starts at `AWAITING_PAYMENT`. Use the current state with the authored agreement JSON to decide which input is valid next. Submit `submitInitialPaymentProof` to move the retainer from `AWAITING_PAYMENT` to `WORK_IN_PROGRESS`. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { submitAgreementInputWithPermit } from '@shodai-network/agreements-api-client'; const clientWallet = createWalletClient({ account: clientRepresentative, chain: lineaSepolia, transport: http(process.env.RPC_URL), }); const inputRecord = await submitAgreementInputWithPermit({ client, agreementId: agreementRecord.id, walletClient: clientWallet, publicClient, chainId: agreementRecord.chainId, agreementContractAddress: agreementRecord.address!, agreement, inputId: 'submitInitialPaymentProof', values: { awaitingPaymentPaymentLink: 'https://sepolia.lineascan.build/tx/0xexample', awaitingPaymentComment: 'Initial payment proof recorded for the tutorial run.', }, }); console.log(inputRecord.status); ``` The signer must be allowed by the input's authored `issuer`. The service retainer allows either representative to submit initial payment proof, but later inputs may be restricted to one role. Reread state and input history after submission. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const nextState = await client.getAgreementState(agreementRecord.id); const inputsPage = await client.listAgreementInputs(agreementRecord.id, { limit: 25, }); console.log({ state: nextState.state, inputs: inputsPage.data.map((input) => ({ inputId: input.inputId, status: input.status, createdAt: input.createdAt, })), }); ``` Use state for the agreement's current lifecycle position and input history as the audit trail of submitted events. ## Try a branch After the agreement reaches `WORK_IN_PROGRESS`, try one additional path to see why the complex example is useful. Submit the `submitInvoice` input from `WORK_IN_PROGRESS`. The service provider representative must sign this input. Required values: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "retainerBalanceBeforeInvoice": 1000, "invoiceLineItems": "2026-04-01,Advisory services,10,100,1000", "submitInvoiceComment": "Invoice submitted for April services." } ``` This path moves the agreement to `INVOICE_SUBMITTED`. From there, an authorized representative can approve, reject with feedback, or initiate termination. Submit the `submitInvoiceWithTopup` input from `WORK_IN_PROGRESS`. The service provider representative must sign this input. Required values: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "retainerBalanceBeforeInvoice": 300, "invoiceLineItems": "2026-04-01,Advisory services,10,100,1000", "topupInvoiceComment": "Invoice submitted with top-up request." } ``` This path moves the agreement to `INVOICE_SUBMITTED_WITH_TOPUP`. The next step can approve with payment proof, reject with feedback, or initiate termination. ## What happened under the hood This tutorial uses one concrete agreement to exercise the core Shodai model. | Tutorial action | System concept | Where to learn more | | ---------------------------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | Load the service retainer JSON | Agreement JSON defines the readable content and executable lifecycle. | [Agreement data standard](/system-architecture/data-standard) | | Validate template | Structural validation checks authored variables, states, inputs, issuers, and transitions. | [Validate Agreement Structure](/workflow/validate-agreement-structure) | | Add participant wallets and `initValues` | Deployment context binds the reusable agreement template to live parties and values. | [Deploy an Agreement](/workflow/deploy-an-agreement) | | Link a participant wallet to an account | Wallet control becomes an account-level identity signal for agreement discovery. | [Link Wallets and Access Agreements](/workflow/link-a-wallet-and-access-agreements) | | Preflight before signing | Preflight normalizes effective values and catches deployment issues before authorization. | [Deploy an Agreement](/workflow/deploy-an-agreement) | | Sign deployment and input permits | EIP-712 signatures prove that an eligible wallet authorized the write. | [EIP-712 Signing Reference](/reference/eip-712-signing) | | Submit `submitInitialPaymentProof` | Inputs are signed lifecycle events accepted only from allowed issuers. | [Author Agreement JSON](/workflow/author-agreement-json) | | Reread state and input history | State shows the current lifecycle position; history shows the submitted event trail. | [Operate a Deployed Agreement](/workflow/operate-a-deployed-agreement) | ## If a step fails Use [Errors and troubleshooting](/reference/errors-and-troubleshooting) for API errors, signing failures, and lifecycle diagnostics. Before retrying a write, reread state and input history so you do not sign or submit against a stale lifecycle position. ## Next steps Study the complete service retainer JSON, lifecycle diagram, states, inputs, and transitions. Turn a business workflow into agreement JSON after you have run the lifecycle once. Use the TypeScript client reference for typed API calls, signing helpers, and diagnostics. Configure the hosted MCP server and operate agreements through MCP tools. # Shodai Reference App Source: https://docs.shodai.network/examples/reference-app Understand how the Shodai Reference App demonstrates a full-stack Agreements API integration with app-owned auth, backend, storage, wallet signing, and reconciliation. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). The Shodai Reference App source lives in the SDK repository at `apps/shodai-reference-app`; use the app README and setup docs for run commands and environment details. The Shodai Reference App is the canonical full-stack example for building a customer-owned application on the Agreements API. It shows how an app can combine its own frontend, backend, auth, storage, template catalog, wallet signing, Agreements API calls, and webhook reconciliation without exposing Shodai API keys or webhook secrets to the browser. Use it when you want to understand the shape of a production-style integration before adapting the patterns to your own stack. ## When to use this example Use this page to understand the full-stack integration model behind the Reference App. Use the SDK repository docs when you are ready to run it locally, configure third-party services, or deploy it. For agreement JSON examples, start with [Simple Agreement](/examples/simple) or [Complex Agreement](/examples/complex). For focused API workflows, use [Deploy an Agreement](/workflow/deploy-an-agreement), [Operate a Deployed Agreement](/workflow/operate-a-deployed-agreement), [Receive webhooks](/webhooks/receive-webhooks), [Agreement activity webhooks](/webhooks/agreement-activity-webhooks), and [Notification webhooks](/webhooks/notification-webhooks). ## What the app demonstrates | Pattern | What to look for | | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Full-stack Agreements API lifecycle | Template selection, draft creation, participant and observer setup, deployment, state viewing, action submission, and activity history. | | Customer-owned boundary | The frontend talks to its own backend. The backend talks to Shodai. | | Server-side secrets | The Nest backend owns the Shodai API key, webhook secret, Dynamic server token, service token, and Mongo credentials. | | Wallet signing | The browser signs deploy and input permits with the connected wallet, then sends signed payloads to the backend. | | Local persistence | The app owns users, contacts, wallets, template access, drafts, agreement mirrors, input mirrors, and webhook event records in Mongo. | | Reconciliation | After writes and webhook deliveries, the backend reads canonical agreement state and input history from the Agreements API. | ## Architecture at a glance | Layer | What it owns | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | React frontend at `/agreements/` | Authenticated product workflow, agreement creation UX, document and state views, and wallet signing for deploy and input permits. | | Nest backend | Auth validation, app-owned API routes, Shodai API key, webhook secret, agreement lifecycle orchestration, and local persistence. | | Mongo | App-owned users, contacts, wallets, template access, drafts, agreement mirrors, input mirrors, webhook receipts, and processing state. | | Shodai Agreements API | Canonical deployed agreement records, state, accepted input history, deployment and input processing, and webhook delivery. | The app runs independently of internal Shodai services. In local development, the backend defaults to `http://localhost:4199` and the frontend defaults to `http://localhost:5184/agreements/`. ## Agreement lifecycle in the app The Reference App models the complete customer application path around the Agreements API: 1. A user signs in through the app's auth flow. 2. The backend mirrors the platform user, contacts, and wallets locally. 3. The frontend loads available agreement templates from the backend. 4. The backend resolves a selected `templateId` against its vendored template catalog. 5. The user configures draft values, participants, observers, display name, and chain. 6. The browser signs an EIP-712 deploy permit with the connected wallet. 7. The backend validates the signer and deploys through the Agreements API using `@shodai-network/agreements-api-client`. 8. The deployed agreement view reads agreement details, state, and inputs through the backend. 9. When an action is available, the browser signs an input permit. 10. The backend submits the input to the Agreements API and refreshes its local mirror. 11. Agreement activity webhooks notify the backend about Shodai-side transitions, and the backend reconciles by reading current data from the Agreements API. 12. Notification webhooks notify the backend when hosted Shodai notification rules fire, and the backend owns final delivery such as SES email. ## App-owned data and Shodai-owned data | App-owned locally | Shodai-owned canonically | | ------------------------------------------------------------------------------- | --------------------------------------------- | | Platform user mirror, contacts, wallets, and auth-adjacent records. | Deployed agreement records. | | Template access and the vendored template catalog. | Canonical agreement state. | | Draft agreements before deployment. | Accepted input history. | | Display names and UX-specific metadata. | Agreement deployment and transition outcomes. | | Local agreement and input mirrors. | Signed webhook deliveries. | | Webhook event receipts, queue status, retry metadata, and dead-letter metadata. | Webhook event payload source. | The app may cache and mirror deployed agreement data, but Shodai remains the source of truth for deployed agreement state and accepted inputs. ## Security boundary Do not place Shodai API keys or webhook secrets in frontend code or `VITE_` environment variables. Keep `EXTERNAL_API_KEY`, `SHODAI_WEBHOOK_SECRET`, Dynamic server tokens, service tokens, and Mongo credentials on the backend. The frontend should only receive browser-safe configuration such as its own backend URL, Dynamic environment ID, supported chains, and RPC configuration. ## Webhook reconciliation in context The webhook receiver is `POST /shodai/webhooks`. For hosted webhook testing against local development, expose backend port `4199` through a public HTTPS tunnel and register the tunnel URL with Shodai. Webhooks are compact events, not full agreement snapshots. The backend verifies, stores, acknowledges, and processes deliveries. Agreement activity webhooks reconcile the local agreement mirror by reading current agreement data from the Agreements API. Notification webhooks trigger final app-owned delivery, such as sending email through SES. For the shared receiver pattern, see [Receive webhooks](/webhooks/receive-webhooks). For event-specific behavior, see [Agreement activity webhooks](/webhooks/agreement-activity-webhooks) and [Notification webhooks](/webhooks/notification-webhooks). ## What you can replace The Reference App is a blueprint, not a required stack. Customers are not required to use Dynamic, Mongo, Nest, React, the vendored template catalog model, or the exact local storage schema. The reusable patterns are: 1. keep Shodai credentials server-side 2. put a customer-owned backend between the browser and the Agreements API 3. let wallets sign deploy and input permits in the browser 4. submit signed payloads through a trusted backend 5. maintain local product state separately from Shodai-owned canonical agreement state 6. reconcile local mirrors from Agreements API reads and webhook deliveries ## Run or inspect the app Use the [SDK repository](https://github.com/CNSLabs/agreements-api-sdk) for setup and deployment details: | Repo path | Use it for | | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `apps/shodai-reference-app` | Reference App source. | | `apps/shodai-reference-app/README.md` | App overview, local requirements, runtime defaults, validation commands, and webhook testing notes. | | `apps/shodai-reference-app/docs/third-party-setup.md` | Local setup details for Dynamic, Shodai API keys, webhook subscriptions, RPC, environment files, Mongo, and validation. | | `apps/shodai-reference-app/docs/deployment.md` | Production-style serving, proxy routes, frontend build path, backend environment, and deployment validation. | The app lives at `apps/shodai-reference-app` in the SDK repo. The frontend is served at `/agreements/`. Local development defaults are backend `http://localhost:4199` and frontend `http://localhost:5184/agreements/`. Webhook testing against hosted Shodai requires a public HTTPS tunnel to backend port `4199`. ## Related pages * [TypeScript client](/sdks/typescript-client) * [Deploy an Agreement](/workflow/deploy-an-agreement) * [Operate a Deployed Agreement](/workflow/operate-a-deployed-agreement) * [Receive webhooks](/webhooks/receive-webhooks) * [Agreement activity webhooks](/webhooks/agreement-activity-webhooks) * [Notification webhooks](/webhooks/notification-webhooks) * [Authentication](/authentication) * [Quickstart with TypeScript SDK](/sdks/quickstart-with-typescript-sdk) * [Run an end-to-end agreement workflow](/examples/end-to-end-workflow) For webhook endpoint request and response schemas, use the Webhooks pages in the API Reference group. # Simple Agreement Source: https://docs.shodai.network/examples/simple Use the smallest complete agreement JSON example to inspect the full document shape without much branching complexity. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). The simple agreement is the smallest complete agreement JSON artifact in these docs. Use it to inspect or adapt the full artifact shape before [validating](/workflow/validate-agreement-structure) and [deploying](/workflow/deploy-an-agreement) with the SDK. ## When to use this example Use this example when you want a complete agreement with minimal branching, or when you need a straightforward starting point for a genuinely linear workflow. For a broader lifecycle with more states, event types, and branching behavior, use [Complex Agreement](/examples/complex). ## What to notice before the canonical agreement JSON Before reading the JSON, notice: 1. participant address variables marked with `subtype: "participant"` 2. rendered `content` that uses the same variables 3. a short set of lifecycle states 4. inputs that correspond to signature and acceptance events 5. transitions that move the agreement forward with minimal branching ## Agreement lifecycle This section renders the simple agreement state machine before the complete deployable JSON artifact. The state machine is: PENDING\_PARTY\_A\_SIGNATURE --partyAData--> PENDING\_PARTY\_B\_SIGNATURE --partyBData--> PENDING\_ACCEPTANCE; PENDING\_ACCEPTANCE --accepted--> ACCEPTED; PENDING\_ACCEPTANCE --rejected--> REJECTED. The diagram below shows the states and transitions defined by this agreement's `execution` object. Use it to understand the workflow before reviewing the full JSON artifact. ## Canonical agreement JSON The following code block is the complete deployable agreement JSON for this example. ```json title="simple-agreement.json" theme={"theme":{"light":"github-light","dark":"github-dark"}} { "metadata": { "id": "did:example:mou-v1", "templateId": "did:template:mou-v1", "version": "1.0.0", "createdAt": "2024-03-20T12:00:00Z", "name": "Memorandum of Understanding", "author": "Agreements Protocol", "description": "Template for non-binding memorandum of understanding between two parties" }, "variables": { "partyAEthAddress": { "type": "address", "subtype": "participant", "name": "Party A Address", "description": "Ethereum address of the first party", "validation": { "required": true } }, "partyAName": { "type": "string", "name": "Party A Name", "description": "Legal name of the first party", "validation": { "required": true, "minLength": 1 } }, "partyASignature": { "type": "string", "subtype": "signature", "name": "Party A Signature", "description": "Digital signature of the first party", "validation": { "required": true } }, "partyBEthAddress": { "type": "address", "subtype": "participant", "name": "Party B Address", "description": "Ethereum address of the second party", "validation": { "required": true } }, "partyBName": { "type": "string", "name": "Party B Name", "description": "Legal name of the second party", "validation": { "required": true, "minLength": 1 } }, "partyBSignature": { "type": "string", "subtype": "signature", "name": "Party B Signature", "description": "Digital signature of the second party", "validation": { "required": true } }, "effectiveDate": { "type": "dateTime", "name": "Effective Date", "description": "The date when this MOU becomes effective", "validation": { "required": true } }, "scope": { "type": "string", "name": "Scope of Cooperation", "description": "The scope of cooperation between the parties", "validation": { "required": true } }, "termDuration": { "type": "string", "name": "Term Duration", "description": "The duration of the agreement", "validation": { "required": true } } }, "content": { "type": "md", "data": "# MEMORANDUM OF UNDERSTANDING\n\n**BETWEEN PARTY A:**\n\n${variables.partyAName} (Party A Name)\n\n${variables.partyAEthAddress} (Party A Address)\n\n**AND PARTY B:**\n\n${variables.partyBName} (Party B Name)\n\n${variables.partyBEthAddress} (Party B Address)\n\n**EFFECTIVE DATE:**\n\n${variables.effectiveDate} (Effective Date)\n\n## 1. INTRODUCTION\n\nThis Memorandum of Understanding (\"MOU\") is entered into by and between Party A and Party B (collectively referred to as the \"Parties\").\n\nThe purpose of this MOU is to identify the roles and responsibilities of each Party.\n\n## 2. SCOPE OF COOPERATION\n\n${variables.scope}\n(Scope)\n\n## 3. RESPONSIBILITIES\n - Maintain regular communication regarding the progress of collaborative activities.\n - Designate representatives to coordinate the implementation of this MOU.\n - Share relevant information and resources necessary for the successful implementation of this MOU.\n - Acknowledge the contribution of the other Party in all public communications related to activities conducted under this MOU.\n\n## 4. TERM AND TERMINATION\n\n4.1 This MOU shall become effective on the date of the last signature below and shall remain in effect for a period of ${variables.termDuration} unless terminated earlier.\n\n4.2 Either Party may terminate this MOU by providing written notice to the other Party.\n\n4.3 Termination of this MOU shall not affect the completion of any activities already in progress, unless otherwise agreed by the Parties.\n\n## 5. CONFIDENTIALITY\n\n5.1 During the course of this MOU, the Parties may share confidential and proprietary information with each other. Each Party agrees to maintain the confidentiality of all information designated as confidential by the disclosing Party and shall not disclose such information to any third party without the prior written consent of the disclosing Party.\n\n## 6. INTELLECTUAL PROPERTY\n\n6.1 This MOU does not transfer any intellectual property rights between the Parties.\n\n6.2 Each Party shall retain all rights, title, and interest in its own intellectual property.\n\n6.3 Any intellectual property created jointly by the Parties during the course of activities under this MOU shall be owned jointly by the Parties, with specific terms to be negotiated in good faith and documented in a separate written agreement.\n\n## 7. SIGNATURES\n\nIN WITNESS WHEREOF, the Parties have executed this Memorandum of Understanding as of the Effective Date.\n\n${variables.partyASignature}\n(Party A Signature)\n\n${variables.partyBSignature}\n(Party B Signature)\n\nBy signing, I confirm that I have read, understood, and agree to be legally bound by all terms of this agreement." }, "execution": { "states": { "PENDING_PARTY_A_SIGNATURE": { "name": "Pending Signature From A", "description": "This state awaits until Party A supplies Party B's address, effective date, scope, duration, and their own name." }, "PENDING_PARTY_B_SIGNATURE": { "name": "Pending Signature From B", "description": "This state awaits until Party B confirms their identity by supplying their name." }, "PENDING_ACCEPTANCE": { "name": "Pending Final Acceptance", "description": "This state awaits Party A's final acceptance of Party B's data." }, "ACCEPTED": { "name": "Agreement Accepted", "description": "The agreement has been accepted by both parties and is now in force." }, "REJECTED": { "name": "Agreement Rejected", "description": "The agreement has been rejected by Party A and will not proceed." } }, "initialize": { "name": "Initialize", "description": "Initialize the agreement", "initialState": "PENDING_PARTY_A_SIGNATURE", "data": { "partyAEthAddress": "${variables.partyAEthAddress}", "partyBEthAddress": "${variables.partyBEthAddress}" } }, "inputs": { "partyAData": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Party A Signature", "description": "EIP712 signature from Party A proposing the MOU terms including scope, duration, and effective date", "data": { "partyAName": "${variables.partyAName}", "scope": "${variables.scope}", "termDuration": "${variables.termDuration}", "effectiveDate": "${variables.effectiveDate}" }, "issuer": "${variables.partyAEthAddress.value}" }, "partyBData": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Party B Signature", "description": "EIP712 signature from Party B accepting the MOU terms", "data": { "partyBName": "${variables.partyBName}", "partyBSignature": "${variables.partyBSignature}" }, "issuer": "${variables.partyBEthAddress.value}" }, "accepted": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Party A Accepted Party B's Data", "description": "EIP712 signature from Party A accepting Party B's data", "data": { "partyASignature": "${variables.partyASignature}" }, "issuer": "${variables.partyAEthAddress.value}" }, "rejected": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Party A Rejected Party B's Data", "description": "EIP712 signature from Party A rejecting Party B's data", "data": { "partyARejectionSignature": { "type": "string", "subtype": "signature", "name": "Party A Rejection Signature", "validation": { "required": true } } }, "issuer": "${variables.partyAEthAddress.value}" } }, "transitions": [ { "from": "PENDING_PARTY_A_SIGNATURE", "to": "PENDING_PARTY_B_SIGNATURE", "conditions": [ { "type": "isValid", "input": "partyAData" } ] }, { "from": "PENDING_PARTY_B_SIGNATURE", "to": "PENDING_ACCEPTANCE", "conditions": [ { "type": "isValid", "input": "partyBData" } ] }, { "from": "PENDING_ACCEPTANCE", "to": "ACCEPTED", "conditions": [ { "type": "isValid", "input": "accepted" } ] }, { "from": "PENDING_ACCEPTANCE", "to": "REJECTED", "conditions": [ { "type": "isValid", "input": "rejected" } ] } ] } } ``` ## Related pages * [Agreement data standard](/system-architecture/data-standard) * [Author Agreement JSON](/workflow/author-agreement-json) * [Complex Agreement](/examples/complex) # Overview Source: https://docs.shodai.network/index Understand how Agreements Protocol gives agreements shared meaning, explicit execution paths, and verifiable history. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). ## Why Agreements Protocol Agreements are how people, organizations, and agents coordinate around shared commitments. ## From static records to operational agreements Traditional agreements are passive records. They describe what parties accepted, but the operational rules usually live somewhere else: in applications, workflows, databases, or custom smart contracts. Shodai makes that operational layer part of the agreement definition itself. A Shodai agreement can define what was agreed, who the parties are, who can act, what inputs are valid, what state the agreement is in, what can happen next, and how the history can be verified. The current implementation realizes this through a common agreement data standard, an onchain execution model, and an API/app layer for deploying and operating agreements. An agreement should not only record what parties accepted; it should define who can act, what can happen next, and how progression can be verified. Today, there is no common language for that operational layer. The document may contain the terms. The workflow tool may coordinate the process. The database may store the status. Application logic may decide what is allowed next. A smart contract, if one exists, may enforce part of the process. The audit trail may live wherever the action happened. That split creates the core problem: there is no shared operational source of truth for what was agreed, who can act, what can happen next, and how the agreement progressed over time. For simple agreements, custom glue can hide that gap. For multi-party workflows, agentic systems, financial commitments, milestone approvals and externally-composed payment flows, grant milestone tracking, approvals, settlement attestations, compliance flows, and onchain coordination, the gap becomes a trust problem. It treats an agreement as a structured artifact that combines human-readable terms with machine-operable state, participant authority, valid inputs, deterministic transitions, and verifiable history. The result is an agreement that humans can review, systems and agents can inspect, and parties can use to interact under shared, verifiable constraints. ## What Agreements Protocol provides Agreements Protocol closes the gap between what parties agree to and how that agreement is reliably, verifiably carried forward. It defines agreements as shared artifacts with: | Capability | What it provides | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Reviewable meaning | Human-readable terms people can review, paired with structured data systems and agents can inspect. | | Authority | Declared participants, replicable templates, immutable onchain deployment. | | Valid interaction | Valid inputs, issuer conditions, explicit states, and deterministic transitions. | | Verifiable history | Deployment records, accepted inputs, state progression, and auditable lifecycle history. | | Extensible execution | Separation between agreement definition and execution engine, with room for profiles, verifiers, actions, SDKs, APIs, and application layers. | These properties are exposed visually as behavioral maps of the agreement, helping humans and agents understand which states exist, which inputs are valid, who may submit them, and what transitions can occur next. The onchain implementation uses a data-defined state machine to make agreement behavior explicit, inspectable, and verifiable. ## Benefit and inspection map | Protocol property | What provides it | Where to inspect | | -------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | Reviewable meaning | Human-readable terms plus structured agreement JSON | [Agreement data standard](/system-architecture/data-standard) | | Explicit authority | Participants, variables, and input issuer rules | [Author Agreement JSON](/workflow/author-agreement-json#author-states-inputs-issuers-and-transitions-as-workflow) | | Valid next actions | Inputs, conditions, and transitions | [Author Agreement JSON](/workflow/author-agreement-json) / [Operate a Deployed Agreement](/workflow/operate-a-deployed-agreement) | | Verifiable history | Onchain state progression and events | [Onchain execution engine](/system-architecture/on-chain) / [EIP-712 Signing Reference](/reference/eip-712-signing) | | Predictable behavior | Fixed deployed definition and deterministic execution | [Deploy an Agreement](/workflow/deploy-an-agreement) / [Onchain execution engine](/system-architecture/on-chain) | | Extensible effects | Actions, modular extensions, and application-layer integrations | [Architecture overview](/system-architecture/overview) | Current boundaries: * the core engine does not currently custody funds or natively transfer ETH/ERC-20 assets * the documented happy path uses the API-assisted deployment and operation flow * direct onchain operation should be documented and inspected through the onchain/EIP-712 references * programmatic agreement generation is possible because agreements are data, but generated agreements should still be valid for the current EVM/onchain implementation and reviewable before deployment Agreements can model payment obligations, approvals, balances, invoices, and settlement attestations. Actual value transfer can be composed through external payment rails, escrow contracts, application-layer integrations, or the modular actions extension point. Shodai has a working prototype for payment-transfer actions, planned to be productized soon alongside other extensions. ## How the system fits together Agreements Protocol moves from agreement meaning to execution to integration across three layers: A common language for terms, variables, participants, inputs, states, transitions, and execution metadata. The onchain implementation uses a data-defined state machine to make agreement behavior explicit, inspectable, and verifiable. Tooling for validation, deployment preflight, participant mapping, signing, state reads, input history, and monitoring. Together, these layers let humans, agents, and software coordinate around agreements with shared meaning, explicit execution paths, and verifiable history. ## Using these docs with an AI agent These docs are designed to help an AI coding agent answer your question, find the right implementation path, and move from explanation to a working experiment. The pages below are useful discovery starting points, not a required reading order: * [llms.txt](https://docs.shodai.network/llms.txt) for the available Markdown pages and OpenAPI. * [Choose an integration surface](/integration-surfaces) for deciding between the TypeScript SDK and MCP first-run paths. * [Quickstart with TypeScript SDK](/sdks/quickstart-with-typescript-sdk) for a TypeScript first flight through auth, validation, preflight, and deploy-permit signing readiness. * [Quickstart with MCP](/sdks/quickstart-with-mcp) for an agent/MCP first flight through authenticated tools and deploy typed-data preparation. * [Run an end-to-end agreement workflow](/examples/end-to-end-workflow) for the full lifecycle after either quickstart. * [TypeScript client reference](/sdks/typescript-client) for SDK methods, signing helpers, wallet/RPC requirements, and diagnostics. * [Architecture](/system-architecture/overview) for the product and protocol model. * [Agreement data standard](/system-architecture/data-standard) for the agreement definition that humans read, agents inspect, and execution engines enforce. After your agent answers your immediate question, ask it to offer the hands-on workflow from [Run an end-to-end agreement workflow](/examples/end-to-end-workflow). The workflow should use MCP when MCP tools are available, use the TypeScript SDK when building or testing a TypeScript integration, and report concrete evidence from validation, preflight, signing, deployment, input submission, state, and input history. Answer the user's immediate question from source docs first. Read any additional docs, generated API reference pages, OpenAPI details, or Markdown exports needed for accuracy. For first experiments, choose between Quickstart with TypeScript SDK and Quickstart with MCP before attempting the end-to-end workflow. For the default testnet SDK flow, create `ApiClient` with `environment: 'testnet'` and the user's API key; do not ask for a custom base URL unless the user explicitly targets a custom host. Report concrete evidence such as validation output, deployment preflight results, signer/deadline/signature fields, agreement ID/address/chain when deployed, submitted input status, state changes, and input-history records. If MCP is involved, distinguish docs/search MCP from Agreements execution MCP. Use docs-domain MCP surfaces only for documentation retrieval. Use `https://shodai.network/mcp` for Agreements tools that author, validate, deploy, or operate agreements. ## Where to go next Start with [Choose an integration surface](/integration-surfaces), then run one first-flight quickstart. After that, use [Run an end-to-end agreement workflow](/examples/end-to-end-workflow) to exercise the full lifecycle. Decide between the TypeScript SDK and MCP first-run paths. Authenticate, validate an example, preflight deployment, and sign a deploy permit without deploying. Configure hosted MCP, call authenticated tools, and prepare deploy typed data without private-key custody. Deploy, submit a signed input, read state, and inspect history. See how agreement definitions, SDKs, onchain execution, history, and APIs fit together. Reference typed methods, signing helpers, diagnostics, path helpers, and exports. ## The default path 1. [Choose an integration surface](/integration-surfaces): choose TypeScript SDK or MCP for the first run. 2. [Quickstart with TypeScript SDK](/sdks/quickstart-with-typescript-sdk) or [Quickstart with MCP](/sdks/quickstart-with-mcp): prove auth plus non-destructive write readiness. 3. [Run an end-to-end agreement workflow](/examples/end-to-end-workflow): run the full lifecycle with the service retainer example. 4. [TypeScript client reference](/sdks/typescript-client): inspect SDK methods, signing helpers, diagnostics, and exports. 5. [Agreement data standard](/system-architecture/data-standard): understand the agreement definition model. 6. [Author agreement JSON](/workflow/author-agreement-json): model the business workflow deliberately. 7. [Validate agreement structure](/workflow/validate-agreement-structure): check the authored artifact before deployment context is added. 8. [Deploy an agreement](/workflow/deploy-an-agreement): preflight deployment values and deploy with a permit signature. 9. [Operate a deployed agreement](/workflow/operate-a-deployed-agreement): read state, submit signed inputs, and inspect history. ## The agreement lifecycle | Phase | What you do | SDK entry point | Start here | | -------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | | Author | Model the agreement text, variables, participants, states, inputs, and transitions. | Agreement JSON artifact | [Author Agreement JSON](/workflow/author-agreement-json) | | Validate | Check authored JSON before combining it with deployment context. | `client.validateTemplate(...)` | [Validate Agreement Structure](/workflow/validate-agreement-structure) | | Deploy | Combine authored JSON with `initValues`, participants, observers, and a deployment permit. | `client.validateDeployment(...)`, `deployAgreementWithPermit(...)` | [Deploy an Agreement](/workflow/deploy-an-agreement) | | Operate | Read current state, submit signed inputs, and confirm lifecycle movement. | `client.getAgreementState(...)`, `submitAgreementInputWithPermit(...)` | [Operate a Deployed Agreement](/workflow/operate-a-deployed-agreement) | ## Where examples and reference fit [Simple Agreement](/examples/simple) and [Complex Agreement](/examples/complex) are complete agreement JSON artifacts you can inspect or adapt before validating and deploying. Use [Run an end-to-end agreement workflow](/examples/end-to-end-workflow) when you want a guided run of the complex service retainer example through validation, deployment, operation, state, and history. Use the API Reference group in the sidebar for generated request and response details from `openapi.json`. Use [EIP-712 Signing Reference](/reference/eip-712-signing) when you are constructing typed data directly, verifying SDK helper behavior, or debugging a signing mismatch. Use [Errors and troubleshooting](/reference/errors-and-troubleshooting) when a request fails. # Choose an integration surface Source: https://docs.shodai.network/integration-surfaces Choose between the TypeScript SDK and MCP for your first Shodai agreement integration. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Choose the first-run path that matches who will drive the workflow. | Surface | Use it when | First success | Start here | | -------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | TypeScript SDK | You are building a TypeScript application or service. | Authenticate, validate an example agreement, preflight deployment, and sign a deploy permit locally without deploying. | [Quickstart with TypeScript SDK](/sdks/quickstart-with-typescript-sdk) | | MCP | You are connecting an AI agent or MCP-capable tool client. | Connect hosted MCP, call authenticated tools, validate an example agreement, and prepare deploy typed data. | [Quickstart with MCP](/sdks/quickstart-with-mcp) | Your integration surface and credential are separate choices. Use an [API key](/authentication#api-keys) when the integration manages a credential for one account. Use [delegated OAuth](/authentication#delegated-oauth) when an application asks a user to connect their Shodai account without sharing an API key. ## What happens after quickstart Both paths converge at the same agreement lifecycle. After one quickstart succeeds, run [Run an end-to-end agreement workflow](/examples/end-to-end-workflow) to deploy an agreement, submit a signed lifecycle input, read state, and inspect input history. ## Where the TypeScript client reference fits Use [TypeScript client reference](/sdks/typescript-client) after first setup when you need constructor options, method details, signing helpers, diagnostics, path helpers, or exports. MCP users who do not already have signing infrastructure can also use the TypeScript SDK with `viem` as the local testnet signing harness for EIP-712 typed data returned by hosted MCP. # Get agreement document Source: https://docs.shodai.network/reference/api/agreement-documents/get-agreement-document /openapi.json get /v0/agreements/documents/{documentId} Returns the rendered prose document associated with an agreement documentId. Access requires the same authorization as reading the agreement record. # Get agreement Source: https://docs.shodai.network/reference/api/agreement-records/get-agreement /openapi.json get /v0/agreements/{id} Returns a single agreement record, including agreement JSON and hosted record context. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Use `client.getAgreement(...)` to retrieve the hosted record and agreement JSON before operating on a deployed agreement. # List agreements Source: https://docs.shodai.network/reference/api/agreement-records/list-agreements /openapi.json get /v0/agreements Lists agreement summaries visible to the authenticated Shodai account. Supports pagination, filtering, and sorting. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Use `client.listAgreements(...)` when integrating with the TypeScript SDK. # Validate agreement structure Source: https://docs.shodai.network/reference/api/authoring/validate-agreement-structure /openapi.json post /v0/agreements/validate-template Checks only the authored agreement JSON and returns participant variable keys, input IDs, state IDs, and warnings. This does not validate deployment values, participant wallet addresses, signer, or permit data. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). This endpoint checks authored agreement JSON only. It does not validate deployment values, participant wallet mappings, signer, or permit data. Generated API examples document request and response shape; complete deployable agreement JSON lives in /examples/simple and /examples/complex. # Deploy agreement Source: https://docs.shodai.network/reference/api/deployment/deploy-agreement /openapi.json post /v0/agreements/deploy-with-permit Deploys authored agreement JSON using an EIP-712 permit. The API submits the on-chain transaction with the signed authorization and returns the deployed agreement record. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Deployment requires an API credential plus a controlled wallet that can produce an EIP-712 signature. Use a `publicClient` connected to the selected `chainId`; the chain must be supported by the target API environment. The `testnet` API environment supports Linea Sepolia, Ethereum Sepolia, and Base Sepolia; the `production` API environment supports Linea Mainnet and Base Mainnet. In SDK integrations, use `deployAgreementWithPermit(...)` after reviewing deployment preflight output. Generated API examples document request and response shape; complete deployable agreement JSON lives in /examples/simple and /examples/complex. # Preflight deployment request Source: https://docs.shodai.network/reference/api/deployment/preflight-deployment-request /openapi.json post /v0/agreements/validate Checks whether authored agreement JSON plus target chain, deployment values, participant wallet mappings, and observer context are ready for deployment. This does not deploy the agreement. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). This endpoint validates the assembled deployment context before any EIP-712 permit is signed. The `testnet` API environment supports Linea Sepolia, Ethereum Sepolia, and Base Sepolia; the `production` API environment supports Linea Mainnet and Base Mainnet. Include the target `chainId` so preflight, signing, and deploy-with-permit requests all use the same supported chain. In SDK integrations, prefer `client.validateDeployment(...)` before `deployAgreementWithPermit(...)`. Generated API examples document request and response shape; complete deployable agreement JSON lives in /examples/simple and /examples/complex. # Get the OpenAPI document for the Agreements API Source: https://docs.shodai.network/reference/api/system/get-the-openapi-document-for-the-agreements-api /openapi.json get /v0/openapi.json Returns the OpenAPI 3.1 specification describing the Agreements API surface. # Health check Source: https://docs.shodai.network/reference/api/system/health-check /openapi.json get /v0/health Public health endpoint for the API gateway. # Get agreement state Source: https://docs.shodai.network/reference/api/using-agreements/get-agreement-state /openapi.json get /v0/agreements/{id}/state Returns the current state of an agreement. For deployed agreements, interpret the state against the authored agreement lifecycle. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Use the current state together with the authored agreement lifecycle before choosing an input to submit. # Get input history Source: https://docs.shodai.network/reference/api/using-agreements/get-input-history /openapi.json get /v0/agreements/{id}/inputs Returns recorded input submissions for the agreement. Use this to inspect what events have been submitted. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Use `client.listAgreementInputs(...)` to inspect submitted events before deciding the next lifecycle action. # Submit input with permit Source: https://docs.shodai.network/reference/api/using-agreements/submit-input-with-permit /openapi.json post /v0/agreements/{id}/input Submits a signed input to a deployed agreement. The input ID and values must match an input defined by the agreement JSON, and the signer must be allowed by that input. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Input submission requires a wallet that can sign for an account allowed by the authored input `issuer`. Use the deployed agreement record's `chainId` and contract address when creating the EIP-712 signature. In SDK integrations, use `submitAgreementInputWithPermit(...)` to sign and submit in one flow. Generated API examples document request and response shape; choose input IDs and values from the deployed agreement JSON. # Create wallet verification challenge Source: https://docs.shodai.network/reference/api/wallet-access/create-wallet-verification-challenge /openapi.json post /v0/siwe/nonce Creates an account-bound EIP-4361 nonce for a wallet address. OAuth bearer tokens require agreements.write; API keys require the equivalent account entitlement. The challenge expires after five minutes, and requesting another nonce invalidates the prior nonce for the same authenticated account and address. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Use an OAuth bearer access token with the `agreements.write` scope, or provide `X-API-Key` as the alternative credential. See [Authentication](/authentication) for delegated OAuth access and [Link a wallet and access agreements](/workflow/link-a-wallet-and-access-agreements) for the creator and participant access model. # Verify and link wallet Source: https://docs.shodai.network/reference/api/wallet-access/verify-and-link-wallet /openapi.json post /v0/siwe/verify Verifies a signed EIP-4361 message and links the wallet to the authenticated Shodai account. OAuth bearer tokens require agreements.write; API keys require the equivalent account entitlement. Verification proves control only: it does not transfer custody or authorize Shodai to sign agreement inputs. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Use an OAuth bearer access token with the `agreements.write` scope, or provide `X-API-Key` as the alternative credential. Verification proves control and links the wallet to the authenticated Shodai account; it does not transfer custody or authorize Shodai to sign agreement inputs. See [Authentication](/authentication) for delegated OAuth access and [Link a wallet and access agreements](/workflow/link-a-wallet-and-access-agreements) for the creator and participant access model. ### TypeScript with viem ```typescript import { createSiweMessage } from 'viem/siwe'; const baseUrl = process.env.SHODAI_API_BASE_URL!.replace(/\/+$/, ''); const apiUrl = new URL(baseUrl); const accessToken = process.env.SHODAI_ACCESS_TOKEN!; const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }; const address = walletClient.account.address; const nonceResponse = await fetch(`${baseUrl}/v0/siwe/nonce`, { method: 'POST', headers, body: JSON.stringify({ address }), }); const { data: challenge } = await nonceResponse.json(); const message = createSiweMessage({ domain: apiUrl.host, address, statement: 'Prove control of this wallet to Shodai.', uri: apiUrl.origin, version: '1', chainId: 59141, nonce: challenge.nonce, issuedAt: new Date(challenge.issuedAt), }); const signature = await walletClient.signMessage({ account: walletClient.account, message, }); const verification = await fetch(`${baseUrl}/v0/siwe/verify`, { method: 'POST', headers, body: JSON.stringify({ address, message, signature, domain: apiUrl.host, chainId: 59141, }), }); console.log(await verification.json()); // X-API-Key: YOUR_API_KEY is the alternative to Authorization. ``` # Create webhook Source: https://docs.shodai.network/reference/api/webhooks/create-webhook /openapi.json post /v0/webhooks Registers a signed webhook endpoint for subscribed agreement activity and notification events visible to the authenticated Shodai account. The signing secret is returned only in the create response. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Create a signed webhook endpoint for agreement activity and notification events. The signing secret is returned only in the create response. # Disable webhook Source: https://docs.shodai.network/reference/api/webhooks/disable-webhook /openapi.json delete /v0/webhooks/{id} Disables a webhook subscription for the authenticated Shodai account. The signing secret is not returned after creation. # Get webhook Source: https://docs.shodai.network/reference/api/webhooks/get-webhook /openapi.json get /v0/webhooks/{id} Returns one webhook subscription for the authenticated Shodai account. The signing secret is not returned after creation. # List webhooks Source: https://docs.shodai.network/reference/api/webhooks/list-webhooks /openapi.json get /v0/webhooks Lists webhook subscriptions for the authenticated Shodai account. Signing secrets are not returned after creation. # Send test webhook Source: https://docs.shodai.network/reference/api/webhooks/send-test-webhook /openapi.json post /v0/webhooks/{id}/test Sends a signed test payload to a webhook subscription. The signing secret is not returned after creation. # Update webhook Source: https://docs.shodai.network/reference/api/webhooks/update-webhook /openapi.json patch /v0/webhooks/{id} Updates a webhook URL, event types, filters, or status. The signing secret is not returned after creation. # EIP-712 Signing Reference Source: https://docs.shodai.network/reference/eip-712-signing Construct low-level EIP-712 typed data when you are not using SDK signing helpers or need to debug permit signatures. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). This page documents the signed authorization layer used by the onchain implementation. The EIP-712 payloads connect offchain agreement definitions and user authorization to onchain deployment and input submission. These signatures are what allow the execution engine to verify that a deployment or input was authorized by the correct party. For the conceptual architecture, see [Architecture](/system-architecture/overview). For the API-assisted deployment and operation flow, see [Workflow](/workflow/deploy-an-agreement). Use this page only when you need the exact low-level signing inputs for `POST /v0/agreements/deploy-with-permit` or `POST /v0/agreements/{id}/input`. For normal TypeScript integrations, prefer the [TypeScript client reference](/sdks/typescript-client): `deployAgreementWithPermit(...)` signs and submits deployment permits, and `submitAgreementInputWithPermit(...)` signs and submits input permits. Use this reference when you are constructing typed data directly, verifying SDK helper behavior, or debugging a signing mismatch. ## Deploy signing reference The deployment permit is signed over derived on-chain parameters, not over the raw deployment request body. If you use `deployAgreementWithPermit(...)`, the SDK performs this derivation and signing for you. For raw API deployment, use the same supported `chainId` in deployment preflight, EIP-712 domain construction, and the deploy-with-permit request body. ### Deploy typed data ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} const domain = { name: "AgreementFactory", version: "1", chainId, verifyingContract: factoryAddress, }; const types = { PermitAgreementWithActions: [ { name: "docUri", type: "string" }, { name: "docHash", type: "bytes32" }, { name: "initialState", type: "bytes32" }, { name: "inputDefsHash", type: "bytes32" }, { name: "transitionsHash", type: "bytes32" }, { name: "initVarsHash", type: "bytes32" }, { name: "verifiersHash", type: "bytes32" }, { name: "actionsHash", type: "bytes32" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" } ] }; const message = { docUri, docHash, initialState, inputDefsHash, transitionsHash, initVarsHash, verifiersHash, actionsHash, nonce, deadline, }; ``` ### Deploy permit nonce Set `message.nonce` to the current value of `AgreementFactory.nonces(signer)` on the target chain before signing. Read it through a live `publicClient`/RPC connected to the same chain as `domain.chainId` and `factoryAddress`; do not hardcode `0`. A successful deploy permit consumes that factory nonce, so deploy signatures are single-use. If the signer nonce changes for any reason, including a previous successful deploy permit, the signature becomes stale and must be regenerated. ### Exact derivation rules ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} docUri = providedDocUri ?? `ipfs://agreement/${agreement.metadata.id}` docHash = keccak256(stringToHex(JSON.stringify(agreement))) initialState = keccak256(stringToHex(agreement.execution.initialize.initialState)) ``` The remaining fields are hashes of ABI-encoded contract parameters: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} inputDefsHash = keccak256(abi.encode(inputDefs)) transitionsHash = keccak256(abi.encode(transitions)) initVarsHash = keccak256(abi.encode(initVars)) verifiersHash = keccak256(abi.encode(verifiers)) actionsHash = keccak256(abi.encode(actions)) ``` Those arrays are constructed from the authored agreement like this: | Array | Source | Derivation | | ------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `inputDefs` | `agreement.execution.inputs` | Each input ID becomes `keccak256(stringToHex(inputId))`; each field ID becomes `keccak256(stringToHex(fieldName))`; issuer constraints become on-chain conditions. | | `transitions` | `agreement.execution.transitions` | `fromState = keccak256(stringToHex(from))`; `toState = keccak256(stringToHex(to))`; `inputId = keccak256(stringToHex(transition.conditions[0].input))`. | | `initVars` | Variables referenced by `agreement.execution.initialize.data` | Each variable ID becomes `keccak256(stringToHex(variableName))`; each variable value is ABI-encoded according to its field type. | | `verifiers` | Verifier registrations supplied for deployment | Each verifier registration binds a verifier key to the verifier contract address installed during agreement initialization. | | `actions` | `agreement.execution.actions` | `fromState` and `inputId` are hashed to bytes32; `target`, `value`, and `data` come from the resolved action call. | Critical rule when participant mappings are present: * Do not sign against raw caller input when participant mappings change any value that is hashed or encoded into the signed message. * Sign against the effective post-mapping values instead. * For deploy, the safe source of truth is the normalized `variables` object returned by `POST /v0/agreements/validate` for the exact deployment payload you plan to submit. For example, if `agreement.execution.initialize.data` references participant-backed variables such as `partyAEthAddress` or `partyBEthAddress`, and those addresses are supplied through `participants`, the final mapped wallet addresses still need to be reflected in the `initVars` used for signing. Field type encoding for `initVars` follows these rules: * `string`, `dateTime`, `signature` → `abi.encode(string)` * `address` → `abi.encode(address)` * `uint256` → `abi.encode(uint256)` * `bool` → `abi.encode(bool)` * `bytes32`, `txHash` → `abi.encode(bytes32)` ### Factory address source The TypeScript helpers resolve the correct factory for the selected chain through the protocol SDK registry. When constructing typed data manually, use the factory address for the same `chainId` that you pass in the EIP-712 domain. Current factory addresses from the protocol SDK registry: * Base Mainnet (`chainId = 8453`): `0x76dAA59C02d902e7063E6328D2E64ACee6CC121e` * Linea Sepolia (`chainId = 59141`): `0x26Ff3AdEC23fC5778f190371B1CcCadDa74e26c8` * Linea Mainnet (`chainId = 59144`): `0xB772Ea12546fd7153Bf1F5ED7266B8faB0dAD6C9` * Base Sepolia (`chainId = 84532`): `0x76dAA59C02d902e7063E6328D2E64ACee6CC121e` * Ethereum Sepolia (`chainId = 11155111`): `0x76dAA59C02d902e7063E6328D2E64ACee6CC121e` For factory and implementation contract addresses with verified source links, see [Contracts](/system-architecture/contracts). ### Signing call ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} const signatureHex = await walletClient.signTypedData({ account, domain, types, primaryType: "PermitAgreementWithActions", message, }); ``` Then split `signatureHex` into: * `r = 0x...` * `s = 0x...` * `v = 27` or `28` If the agreement JSON, `initValues`, `docUri`, chain, factory address, signer nonce, or deadline changes, the signature must be regenerated. The TypeScript client uses a one-hour default permit lifetime through `computeDefaultDeadlineSeconds()`. Low-level signing calls still require you to pass an explicit `deadline`. ## Input signing reference The input permit is signed over the hashed input ID and an ABI-encoded payload. If you use `submitAgreementInputWithPermit(...)`, the SDK performs this derivation and signing for you. For raw input submission, read the deployed agreement record first and use its `chainId` and contract address in the EIP-712 domain. ### Input typed data ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} const domain = { name: "AgreementEngine", version: "1", chainId, verifyingContract: agreementAddress, }; const types = { PermitInput: [ { name: "inputId", type: "bytes32" }, { name: "payload", type: "bytes" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" } ] }; const message = { inputId, payload, nonce, deadline, }; ``` ### Exact derivation rules ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} inputIdBytes32 = keccak256(stringToHex(inputId)) payload = abi.encode(dataFields) ``` Where `dataFields` has this contract shape: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} type DataField = { id: bytes32, fType: uint8, data: bytes, } ``` Build it from the authored input schema like this: | Step | Derivation | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Look up the input schema | Read `agreement.execution.inputs[inputId].data`. | | Encode each submitted field | `id = keccak256(stringToHex(fieldName))`; `fType =` the on-chain field type mapped from the authored variable type; `data =` ABI-encoded field value. | | Encode the array | ABI-encode the whole array as `(bytes32 id, uint8 fType, bytes data)[]`. | ### Field type mapping * `uint256` → `UINT256` * `string` → `STRING` * `address` → `ADDRESS` * `bool` → `BOOL` * `bytes32` → `BYTES32` * `signature` → `STRING` * `dateTime` → `STRING` * `txHash` → `BYTES32` ### Field value encoding rules * `STRING` → `abi.encode(string)` * `ADDRESS` → `abi.encode(address)` * `UINT256` → `abi.encode(uint256)` * `BOOL` → `abi.encode(bool)` * `BYTES32` → `abi.encode(bytes32)` ### Effective signing pipeline ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} fieldId = keccak256(stringToHex(fieldName)) encodedValue = abi.encode(valueForThatFieldType) payload = abi.encode([{ id: fieldId, fType, data: encodedValue }, ...]) message = { inputId: inputIdBytes32, payload, nonce, deadline } ``` ### Signing call ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} const signatureHex = await walletClient.signTypedData({ account, domain, types, primaryType: "PermitInput", message, }); ``` Then split `signatureHex` into: * `r = 0x...` * `s = 0x...` * `v = 27` or `28` If `inputId`, `values`, agreement schema, agreement address, signer nonce, chain, or deadline changes, the signature must be regenerated. ## Related pages * [Architecture](/system-architecture/overview) * [TypeScript client reference](/sdks/typescript-client) * [Deploy an Agreement](/workflow/deploy-an-agreement) * [Operate a Deployed Agreement](/workflow/operate-a-deployed-agreement) * [Errors and troubleshooting](/reference/errors-and-troubleshooting) # Errors and troubleshooting Source: https://docs.shodai.network/reference/errors-and-troubleshooting Resolve common API authentication, entitlement, validation, signing, deployment, and input-submission failures. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Use this page when an API request fails or when an agreement does not move through the lifecycle as expected. ## HTTP status codes Error responses use a top-level `error` object. Use `error.requestId` when sharing a failure with support. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "error": { "code": "bad_request", "message": "limit must be an integer between 1 and 100", "details": { "field": "limit" }, "requestId": "req_123" } } ``` | Status | Meaning | Common cause | | ------ | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | The request is malformed or semantically invalid. | Invalid status filter, deployment payload, agreement JSON, or signed input payload. | | `401` | Missing, invalid, expired, or unsupported API credential. | The API key is absent, revoked, disabled, or for another environment; or the OAuth access token is expired, invalid, or issued by the wrong environment. See [Authentication](/authentication). | | `402` | The authenticated Shodai account has `paid_required` entitlement mode for the requested scope. | Per-call x402 settlement is not implemented. Treat this as an entitlement/operator issue. | | `403` | Authenticated but not allowed. | Missing active entitlement, blocked entitlement, or resource access denial. | | `404` | Requested agreement or resource was not found. | Wrong agreement ID, wrong deployed address, or unavailable record. | | `429` | Rate limited. | Too many requests in a short window; back off before retrying. | Use the API Reference group in the sidebar for endpoint-specific response schemas. ## Downstream conflict responses Some deployment or input operations may surface `409 Conflict` from the downstream agreements service. This note is separate from the main status table; use the API Reference group for generated endpoint-specific response codes. | Status | Where it may surface | Common cause | Next check | | ------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | | `409` | Deployment or input operations. | The record is in the wrong lifecycle status for the operation, or the live state no longer accepts the submitted action. | Reread the agreement record and current state, then confirm the requested operation is valid for that status and state. | ## Validate the right thing `POST /v0/agreements/validate-template` checks authored agreement JSON only. If it fails or returns warnings, inspect participant variable keys, input IDs, state IDs, and agreement structure before preparing deployment. `POST /v0/agreements/validate` checks the assembled deployment request. If it fails, compare the authored agreement with `initValues`, participant wallet mappings, observers, and the normalized `variables` response. Deployment preflight does not deploy the agreement and does not validate permit signatures. ## Fix signing failures Regenerate the signature if any of these values change: * agreement JSON * `initValues` * `docUri` * chain * factory or agreement address * signer nonce * deadline * input ID * input values For deployment, sign the effective post-mapping values returned by `POST /v0/agreements/validate`, not raw caller input, when participant mappings change values included in the signature. The TypeScript client uses a one-hour default permit lifetime through `computeDefaultDeadlineSeconds()`. Use a shorter deadline if your integration requires a tighter replay window, and regenerate the signature whenever the deadline expires. ## Diagnose errors in the TypeScript client `ApiClient` throws `AgreementsApiError` for unexpected HTTP responses. Inspect `status`, `errorPayload`, `bodyText`, and `parsedBody` before retrying. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { AgreementsApiError } from '@shodai-network/agreements-api-client'; try { await client.validateDeployment(payload); } catch (error) { if (error instanceof AgreementsApiError) { console.error({ status: error.status, message: error.errorPayload?.error.message, code: error.errorPayload?.error.code, requestId: error.errorPayload?.error.requestId, body: error.parsedBody, }); } throw error; } ``` Use `client.exchangeJson('GET', '/v0/agreements')` when you need `status`, `ok`, `headers`, `bodyText`, and `parsedBody` without throwing on HTTP errors. For successful raw HTTP responses, inspect `data` first. List responses also include `pageInfo`; single-resource responses include only `data` and `meta`. ## Input does not move the agreement Check these conditions in order: 1. The agreement is in the state that accepts the input. 2. The submitted `inputId` exists in the authored agreement. 3. The submitted `values` match the input schema. 4. The signer is allowed by the input `issuer`. 5. The transition condition references that input from the current state. 6. The signature has not expired and was generated for this exact payload. If an input record is `PENDING` and state has not updated yet, the transaction has been submitted but is not yet finalized on chain; state advances when the input becomes `FINALIZED`. Reread `GET /v0/agreements/{id}/state` after a short delay, subscribe to `agreement.transitioned` to be told when it happens, and use `GET /v0/agreements/{id}/inputs` as the audit trail. ## Deployment conflicts For deployment and operation failures, confirm that: 1. the agreement ID or deployed address points to the intended record 2. the authenticated Shodai account can access that record 3. the requested operation is valid for the agreement's current lifecycle position 4. the current state still accepts the action you are submitting 5. the signing wallet is authorized for the requested action ## Related pages * [Authentication](/authentication) * [Validate Agreement Structure](/workflow/validate-agreement-structure) * [Deploy an Agreement](/workflow/deploy-an-agreement) * [Operate a Deployed Agreement](/workflow/operate-a-deployed-agreement) * [TypeScript client reference](/sdks/typescript-client) * [EIP-712 Signing Reference](/reference/eip-712-signing) # May 2026 API/SDK Response Migration Source: https://docs.shodai.network/reference/migrations/may-2026-api-sdk-response-migration Update Agreements API integrations for response envelopes, paged list results, filtering, sorting, and normalized error payloads. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Effective date: 2026-05-20. Use this guide when updating code that reads agreement records or input history from the Agreements API or `@shodai-network/agreements-api-client`. ## What changed Authenticated agreement routes return response envelopes. Raw single-resource HTTP responses use `data` and `meta`; raw list responses use `data`, `pageInfo`, and `meta`. | Route type | Raw HTTP success shape | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------- | | Single-resource routes | `{ "data": ..., "meta": { "apiVersion": "v0", "requestId": "..." } }` | | List routes | `{ "data": [...], "pageInfo": { "limit": 25, "nextCursor": "..." }, "meta": { "apiVersion": "v0", "requestId": "..." } }` | | Health check | Unchanged: `{ "status": "ok", "service": "external-api", "timestamp": "..." }` | The TypeScript client unwraps single-resource methods such as `getAgreement(...)`, `validateTemplate(...)`, `validateDeployment(...)`, `deployWithPermit(...)`, `getAgreementState(...)`, and `submitAgreementInput(...)`. List methods return the list envelope so callers can read `data`, `pageInfo`, and `meta`. ## Upgrade checklist 1. Upgrade to `@shodai-network/agreements-api-client` `0.2.0` or newer. This is the minimum version for the response-envelope changes described here. 2. Update raw HTTP consumers to read resource responses from `response.data`. 3. Update list consumers to iterate over `response.data`. 4. Use `response.pageInfo.nextCursor` for the next page when present. 5. Store or log `response.meta.requestId` when support traceability matters. 6. Update error handling to read `error.code`, `error.message`, optional `error.details`, and `error.requestId`. ## SDK before and after Before `0.2.0`, list methods returned arrays: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const agreements = await client.listAgreements(); console.log(agreements[0].id); const inputs = await client.listAgreementInputs(agreementId); console.log(inputs.length); ``` In `0.2.0`, list methods return paged envelopes: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const agreementsPage = await client.listAgreements({ limit: 25 }); console.log(agreementsPage.data[0]?.id); console.log(agreementsPage.pageInfo.nextCursor); const inputsPage = await client.listAgreementInputs(agreementId, { limit: 25 }); console.log(inputsPage.data.length); ``` Single-resource SDK methods continue to return the resource directly: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const agreement = await client.getAgreement(agreementId); const state = await client.getAgreementState(agreementId); ``` Agreement list items are summaries. Use `getAgreement(id)` when you need full agreement JSON, participants, observers, variables, or on-chain context. ## Query parameters Agreement lists support `limit`, `cursor`, `state`, `createdAt`, `updatedAt`, and one `sort` field from `createdAt`, `updatedAt`, or `displayName`. Input history lists support `limit`, `cursor`, `userId`, `inputId`, `status`, `createdAt`, `updatedAt`, and one `sort` field from `createdAt` or `updatedAt`. Date filters use bracket operators: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl --globoff -sS "$BASE_URL/v0/agreements?createdAt[gte]=2026-05-01T00:00:00.000Z&sort[createdAt]=desc&limit=25" \ -H "X-API-Key: $API_KEY" ``` Use `--globoff` with `curl` when sending bracket query parameters. ## Error responses Errors use a top-level `error` object: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "error": { "code": "bad_request", "message": "Unsupported query parameter \"foo\"", "requestId": "req_123" } } ``` Branch on `error.code` and include `error.requestId` when reporting a failure. ## Related pages * [Quickstart with TypeScript SDK](/sdks/quickstart-with-typescript-sdk) * [TypeScript client reference](/sdks/typescript-client) * [Operate a Deployed Agreement](/workflow/operate-a-deployed-agreement) * [Errors and troubleshooting](/reference/errors-and-troubleshooting) # May 2026 Multi-Chain Migration Source: https://docs.shodai.network/reference/migrations/may-2026-multi-chain-migration Update Agreements API integrations for explicit deployment chain selection, multi-chain signing, and the 0.3.0 TypeScript client changes. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Effective date: 2026-05-26. Use this guide when upgrading an integration that deploys agreements, signs EIP-712 permits, submits agreement inputs, or filters agreement lists with `@shodai-network/agreements-api-client@0.3.0`. ## What changed Agreements can now be deployed to more than one supported EVM chain in the same hosted API environment. Deployment, signing, and input submission code must carry the selected `chainId` consistently instead of assuming a single environment chain. | Package | Upgrade target | Why it matters | | ----------------------------------------------- | -------------- | --------------------------------------------------------------------------------------- | | `@shodai-network/agreements-api-client@0.3.0` | `0.3.0` | Adds multi-chain signing checks and requires `chainId` for input signing helpers. | | `@shodai-network/agreements-protocol-evm@0.1.4` | `0.1.3` | Adds protocol deployment registry entries for Base, Base Sepolia, and Ethereum Sepolia. | Supported hosted deployment chains: | API environment | Supported chains | | --------------- | ------------------------------------------------------------------------------ | | `testnet` | Linea Sepolia (`59141`), Ethereum Sepolia (`11155111`), Base Sepolia (`84532`) | | `production` | Linea Mainnet (`59144`), Base Mainnet (`8453`) | ## Upgrade checklist 1. Upgrade to `@shodai-network/agreements-api-client@0.3.0`. 2. Confirm your lockfile resolves `@shodai-network/agreements-protocol-evm@0.1.4` or newer. 3. Choose a supported `chainId` before deployment preflight. 4. Include the same `chainId` in `client.validateDeployment(...)`, EIP-712 deploy signing, and deploy-with-permit requests. 5. Create the `publicClient` and `walletClient` for the selected chain before signing. 6. For input signing, read the deployed agreement record and pass `agreementRecord.chainId` to `submitAgreementInputWithPermit(...)` or `signAgreementInputPermit(...)`. 7. Add `chainId` filters to agreement list views when your UI or job should operate on one chain at a time. 8. Update error handling for unsupported chains and RPC/client chain mismatches. ## Deployment code changes Before this migration, integrations often treated the API environment as the chain selection: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const validation = await client.validateDeployment({ agreement, initValues, participants, }); const deployed = await deployAgreementWithPermit({ client, walletClient, publicClient, agreement, displayName: 'Consulting Agreement', initValues: validation.variables, participants, }); ``` After the migration, select a supported `chainId` and carry it through the whole deployment flow: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const chainId = 59141; const validation = await client.validateDeployment({ agreement, chainId, initValues, participants, }); const deployed = await deployAgreementWithPermit({ client, walletClient, publicClient, chainId, agreement, displayName: 'Consulting Agreement', initValues: validation.variables, participants, }); ``` `publicClient.getChainId()` must match the selected `chainId`. If it does not, the SDK rejects before requesting a signature. ## Input signing changes The `0.3.0` TypeScript client makes `chainId` required for input signing helpers. Use the chain stored on the deployed agreement record, not a hardcoded environment default. Before `0.3.0`: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} await submitAgreementInputWithPermit({ client, agreementId, walletClient, publicClient, agreementContractAddress, agreement, inputId: 'partyASignature', values, }); ``` In `0.3.0`: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const agreementRecord = await client.getAgreement(agreementId); await submitAgreementInputWithPermit({ client, agreementId: agreementRecord.id, walletClient, publicClient, chainId: agreementRecord.chainId, agreementContractAddress: agreementRecord.address!, agreement: agreementRecord.json as AgreementJson, inputId: 'partyASignature', values, }); ``` The raw HTTP input request body does not include `chainId`. The API uses the stored agreement record after lookup. `chainId` is required by the SDK signing helpers so the EIP-712 domain is built for the deployed agreement's chain and the RPC client can be checked before signing. ## Raw HTTP changes Deployment preflight and deploy-with-permit requests should include `chainId`: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "agreement": {}, "chainId": 59141, "initValues": {}, "participants": [] } ``` ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "agreement": {}, "displayName": "Consulting Agreement", "chainId": 59141, "signer": "0x1111111111111111111111111111111111111111", "deadline": 1776219513, "signature": { "v": 27, "r": "0x...", "s": "0x..." } } ``` For raw EIP-712 signing, use the same `chainId` in the typed-data domain and use the factory address registered for that chain. For input signing, read `GET /v0/agreements/{id}` first and use the returned `chainId` and `address`. ## Agreement list filtering Agreement lists can now be filtered by chain: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const page = await client.listAgreements({ chainId: 84532, limit: 25, }); ``` Raw HTTP: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -sS "$BASE_URL/v0/agreements?chainId=84532&limit=25" \ -H "X-API-Key: $API_KEY" ``` If you omit the filter, list responses can include agreements from every chain visible to the API key. ## Protocol registry changes The protocol SDK registry now includes five deployments. Use registry helpers instead of hardcoding factory addresses: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { getFactoryConfigByChainId } from '@shodai-network/agreements-protocol-evm'; const factoryConfig = getFactoryConfigByChainId(chainId); if (!factoryConfig) { throw new Error(`Unsupported agreement chain: ${chainId}`); } ``` This matters even when two networks share the same factory address. The EIP-712 domain still needs the correct `chainId`. ## Common upgrade failures | Symptom | Likely cause | Fix | | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `Requested chainId ... does not match publicClient chainId ...` | The RPC client is connected to a different chain than the selected agreement chain. | Recreate `publicClient` for the selected `chainId` before signing. | | `No AgreementFactory deployment registered for chain ...` | The selected chain is not in the installed protocol SDK registry. | Upgrade to `@shodai-network/agreements-protocol-evm@0.1.4` or choose a supported chain. | | `Unsupported agreement.chainId ...` | The API environment does not allow deployments on that chain. | Choose one of the chains supported by the target API environment. | | Input signature is rejected or does not advance state | The signature was created for the wrong chain, contract address, payload, signer, nonce, or deadline. | Re-read the agreement record and current state, then sign again with `agreementRecord.chainId` and `agreementRecord.address`. | ## Related pages * [TypeScript client reference](/sdks/typescript-client) * [Deploy an Agreement](/workflow/deploy-an-agreement) * [Operate a Deployed Agreement](/workflow/operate-a-deployed-agreement) * [EIP-712 Signing Reference](/reference/eip-712-signing) * [Contracts](/system-architecture/contracts) * [May 2026 API/SDK Response Migration](/reference/migrations/may-2026-api-sdk-response-migration) # Connect an installed TypeScript client with delegated OAuth Source: https://docs.shodai.network/sdks/delegated-oauth-with-typescript Sign a Shodai user into an installed Node.js client, restore their session, authenticate API calls, and disconnect safely. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Use this workflow when you are building a Node.js CLI or desktop-style installed client in which a Shodai user signs in. The authenticated `ApiClient` calls the Agreements API directly with that user's delegated access token. ## Prerequisites * Node.js `>=18`. * A public OAuth client ID that is already registered in the Developer Portal. * The exact loopback redirect `http://127.0.0.1/callback` registered for that client. * The authorization-server issuer and Agreements API base URL for the same Shodai environment. ## Install the SDK ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install @shodai-network/agreements-api-client ``` Import `ApiClient` from the package root and the delegated session types from the Node-only `/oauth` export. ## Create and restore the session The following example stores the complete rotated token set in an application-owned file. Replace these file operations with your platform's secure storage when appropriate. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { ApiClient } from '@shodai-network/agreements-api-client'; import { OauthDelegatedSession, type OauthDelegatedTokenSet, } from '@shodai-network/agreements-api-client/oauth'; const clientId = requiredEnv('OAUTH_CLIENT_ID'); const issuer = requiredEnv('OAUTH_ISSUER_URL'); const apiBaseUrl = requiredEnv('EXTERNAL_API_BASE_URL'); const tokenPath = resolve(process.env.SHODAI_TOKEN_PATH ?? '.shodai-oauth-session.json'); async function loadTokens(): Promise { try { return JSON.parse(await readFile(tokenPath, 'utf8')) as OauthDelegatedTokenSet; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined; throw error; } } async function saveTokens(tokens: OauthDelegatedTokenSet): Promise { await mkdir(dirname(tokenPath), { recursive: true }); await writeFile(tokenPath, `${JSON.stringify(tokens, null, 2)}\n`, { mode: 0o600 }); } async function clearTokens(): Promise { await rm(tokenPath, { force: true }); } function requiredEnv(name: string): string { const value = process.env[name]?.trim(); if (!value) throw new Error(`Set ${name} before running this client.`); return value; } const session = new OauthDelegatedSession({ clientId, issuer, scope: 'agreements.read agreements.write', onTokensUpdated: saveTokens, onTokensCleared: clearTokens, }); const savedTokens = await loadTokens(); if (savedTokens) { session.restoreTokens(savedTokens); } if (process.argv.includes('--disconnect')) { if (!savedTokens) { console.log('No saved session to disconnect.'); } else { try { await session.revoke(); console.log('Disconnected and cleared the saved session.'); } catch (error) { console.error('Local token cleanup or remote revocation was not confirmed.', error); process.exitCode = 1; } } } else { if (!savedTokens) { await session.loginWithLoopback(); } const client = new ApiClient({ baseUrl: apiBaseUrl, tokenProvider: session.tokenProvider(), }); const agreements = await client.listAgreements({ limit: 5 }); console.log(`Signed in; ${agreements.data.length} agreement(s) returned.`); } ``` Set the environment-matched values, then run the application. On the first run, `loginWithLoopback()` opens the consent page and waits for the registered loopback redirect. Later runs restore the saved token set, and `tokenProvider()` refreshes an expired access token when a refresh token is available. Run the same application with `--disconnect` when the user disconnects. ## Confirm the result A successful first run opens the consent page, saves the delegated token set after authorization, and prints an authenticated Agreements API result. A later run reuses the saved session without opening the consent page. The lifecycle callbacks are both required for durable sessions: * `onTokensUpdated` saves the complete token set after login and refresh-token rotation. * `restoreTokens()` restores that saved set before the first API call. * `onTokensCleared` removes the saved set when `revoke()` disconnects the user. * `revoke()` clears in-memory state and attempts persisted-state deletion even when server-side revocation cannot be confirmed. Treat a rejected `revoke()` as an incomplete disconnect: local storage deletion may have failed, remote revocation may be unconfirmed, or both failures may be present in an `AggregateError`. Keep the error visible to the user or operator instead of reporting an unconditional successful disconnect. For constructor fields and exported symbols, see the [TypeScript client reference](/sdks/typescript-client). # Quickstart with MCP Source: https://docs.shodai.network/sdks/quickstart-with-mcp Connect hosted MCP at https://shodai.network/mcp with browser OAuth, validate an agreement, and prepare testnet deployment typed data. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Do not confuse the docs-domain MCP server with the Agreements execution MCP server. Any MCP card or MCP endpoint on `docs.shodai.network` is for documentation search and retrieval only. To author, validate, deploy, or operate agreements, configure the Agreements execution MCP server at `https://shodai.network/mcp`. Use this quickstart to connect an OAuth-capable AI client to hosted Shodai MCP, validate a complete agreement JSON artifact, preflight deployment values, and prepare deploy EIP-712 typed data on testnet. You will authenticate through the browser without giving hosted MCP a private key, and you will stop before a live write. For a TypeScript app or service, use [Quickstart with TypeScript SDK](/sdks/quickstart-with-typescript-sdk). To compare the two first-run paths, start with [Choose an integration surface](/integration-surfaces). This quickstart uses Shodai's free testnet environment. Production access is available by request. [Request production access](https://developers.shodai.network/support). This quickstart stops after `prepare_deployment_typed_data`. The returned typed data proves that hosted MCP can assemble the exact deploy authorization payload, including chain nonce/context, without performing a live write. ## Connect hosted MCP with browser OAuth Hosted Shodai MCP uses protected-resource discovery so an OAuth-capable client can locate Shodai's authorization server and start browser authorization automatically. Add `https://shodai.network/mcp` as a remote Streamable HTTP MCP server. Connect to the server and allow your MCP client to open Shodai sign-in in the browser. Sign in, review the permissions requested by the client, and approve the connection. Return to your MCP client and confirm that the Shodai Agreements tools are available. The hosted server is stateless. Each API-calling tool requires `environment: "testnet"` or `environment: "production"`, and this quickstart uses `testnet` throughout. Browser OAuth discovery for `https://shodai.network/mcp` currently connects to the testnet authorization server. That OAuth connection works only with `environment: "testnet"`; using it with `environment: "production"` returns `401`. To call production tools through hosted MCP, provide a production API key as `Authorization: Bearer cns_pk_...` until production MCP OAuth is enabled. Send an API key as `Authorization: Bearer cns_pk_...`. The key must belong to the same environment passed to each tool, such as a testnet key with `environment: "testnet"`. See [Authentication](/authentication) for key creation, storage, production provisioning, and failure details. ## Validate and prepare an agreement Call: ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} list_agreements ``` with: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "environment": "testnet", "limit": 25 } ``` This confirms the authenticated MCP connection can call the testnet Agreements API. Run `resources/list`, then read: ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} agreements://examples/simple-agreement.json ``` Parse the returned `contents[0].text` value as `agreement` for the remaining tool calls. Call: ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} validate_agreement ``` with: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "environment": "testnet", "agreement": "" } ``` Confirm the response includes participant variable keys, input IDs, state IDs, and warnings. Choose public wallet addresses for the first-flight deployment context. Hosted MCP needs addresses only, not private keys. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "chainId": 59141, "signerAddress": "0x1111111111111111111111111111111111111111", "participants": [ { "variableKey": "partyAEthAddress", "walletAddress": "0x1111111111111111111111111111111111111111" }, { "variableKey": "partyBEthAddress", "walletAddress": "0x2222222222222222222222222222222222222222" } ] } ``` Use real public addresses from your signing setup for an actual run. The first address is the deploy signer for this first-flight check. Call: ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} preflight_deployment ``` with: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "environment": "testnet", "agreement": "", "chainId": 59141, "participants": [ { "variableKey": "partyAEthAddress", "walletAddress": "0x1111111111111111111111111111111111111111" }, { "variableKey": "partyBEthAddress", "walletAddress": "0x2222222222222222222222222222222222222222" } ] } ``` Review `variables`, `participants`, `observers`, `contributors`, and `warnings` before preparing typed data. Call: ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} prepare_deployment_typed_data ``` with the same `environment`, `agreement`, `chainId`, `signerAddress`, and `participants`: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "environment": "testnet", "agreement": "", "chainId": 59141, "signerAddress": "0x1111111111111111111111111111111111111111", "participants": [ { "variableKey": "partyAEthAddress", "walletAddress": "0x1111111111111111111111111111111111111111" }, { "variableKey": "partyBEthAddress", "walletAddress": "0x2222222222222222222222222222222222222222" } ] } ``` Confirm the response includes: * `typedData` * `signerAddress` * `chainId` * `deadline` * `docUri` * `documentId` * `normalizedInitValues` * `normalizedParticipants` * `normalizedObservers` * `preflightWarnings` * `nextStep` * `playgroundUrl` ## Recommended signing harness Hosted MCP prepares the exact EIP-712 typed data, but signing remains external. If your MCP client or agent host does not already have a wallet, signing service, or `eth_signTypedData_v4` flow, install `@shodai-network/agreements-api-client` and `viem` locally and use the TypeScript SDK as the happy-path testnet signing harness. The signing harness is optional because hosted MCP does not require one specific custody model. Use one of these signing paths: | Signing path | Use it when | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | Existing wallet or signing service | Your agent host already exposes an EIP-712 signer. | | TypeScript SDK + `viem` | You need a local testnet signing harness for the typed data returned by MCP. | | API Playground | You want browser-based experimentation with a key you control. | | Local stdio MCP signer | You are doing development/testnet automation with `AGREEMENTS_SIGNER_PRIVATE_KEY`; do not use this for production keys. | For SDK signing details, see [TypeScript client reference](/sdks/typescript-client). For low-level payload semantics and debugging, see [EIP-712 signing](/reference/eip-712-signing). ## Write authority Only `deploy_agreement` and `submit_input` are side-effecting tools; `submit_input` may advance lifecycle state. `validate_agreement`, `preflight_deployment`, `prepare_deployment_typed_data`, and `prepare_input_typed_data` are non-destructive preparation steps. Scope is not the same as side effect: some non-destructive tools require `agreements.write` because they validate deployment context or prepare write authorization. Hosted MCP receives signed permit fields only. It never receives private keys. `AGREEMENTS_SIGNER_PRIVATE_KEY` is local stdio-only and for development/testnet automation. ## Run the full lifecycle After this first flight works, continue to [Run an end-to-end agreement workflow](/examples/end-to-end-workflow). That tutorial shows how the MCP and TypeScript SDK paths converge for live deployment, signed input submission, state reads, and input history. # Quickstart with TypeScript SDK Source: https://docs.shodai.network/sdks/quickstart-with-typescript-sdk Install the TypeScript client, authenticate with an API key, validate an example agreement, and prove EIP-712 signing readiness without deploying. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Use this quickstart when you are building a TypeScript app or service with Shodai Agreements. You will authenticate with the Agreements API, validate a complete agreement JSON artifact, preflight deployment values, and sign the deploy permit locally without creating a live agreement. For the agent/MCP path, use [Quickstart with MCP](/sdks/quickstart-with-mcp). To compare the two first-run paths, start with [Choose an integration surface](/integration-surfaces). If you are building an installed Node.js CLI or desktop-style client in which a Shodai user signs in, use [Connect an installed TypeScript client with delegated OAuth](/sdks/delegated-oauth-with-typescript). The API-key flow below remains the shortest path for other TypeScript integrations. This quickstart stops before `deployWithPermit(...)`. The final signing step proves that your app can reach API auth, validation, deployment preflight, chain/RPC context, and EIP-712 signing without performing a live write. ## Prerequisites * Node.js `>=18`. * A Shodai testnet API key. Create one in the [Developer Portal](https://developers.shodai.network/portal). * A Linea Sepolia RPC URL for `chainId: 59141`. * A local package or temporary directory where you can install npm packages. ## Make the first-flight script ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} mkdir shodai-sdk-quickstart cd shodai-sdk-quickstart npm init -y npm pkg set type=module ``` ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install @shodai-network/agreements-api-client viem npm install --save-dev tsx ``` The API client handles typed Agreements API calls. `viem` provides the test wallet, public client, and EIP-712 signing primitives used by the SDK signing helpers. ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} export API_KEY="api_key_replace_me" export RPC_URL="linea_sepolia_rpc_url_replace_me" ``` `RPC_URL` must point at Linea Sepolia because this quickstart uses `chainId: 59141`. Copy the complete `simple-agreement.json` code block from [Simple Agreement](/examples/simple) into: ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} simple-agreement.json ``` Use the complete JSON artifact from the example page, not an abbreviated API request body. Create `quickstart.ts`: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { readFile } from 'node:fs/promises'; import { ApiClient, computeDefaultDeadlineSeconds, signDeployWithPermit, } from '@shodai-network/agreements-api-client'; import { createPublicClient, createWalletClient, http } from 'viem'; import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { lineaSepolia } from 'viem/chains'; const apiKey = process.env.API_KEY; const rpcUrl = process.env.RPC_URL; if (!apiKey) throw new Error('Set API_KEY before running this script.'); if (!rpcUrl) throw new Error('Set RPC_URL to a Linea Sepolia RPC endpoint.'); const chainId = 59141; const client = new ApiClient({ environment: 'testnet', apiKey, }); const health = await client.getHealth(); console.log('Health:', health.status, health.service); const agreementsPage = await client.listAgreements({ limit: 25 }); console.log('Visible agreements:', agreementsPage.data.length); const agreement = JSON.parse( await readFile(new URL('./simple-agreement.json', import.meta.url), 'utf8'), ); const templateValidation = await client.validateTemplate(agreement); console.log('Template validation:', { participantVariableKeys: templateValidation.participantVariableKeys, inputIds: templateValidation.inputIds, stateIds: templateValidation.stateIds, warnings: templateValidation.warnings, }); const partyA = privateKeyToAccount(generatePrivateKey()); const partyB = privateKeyToAccount(generatePrivateKey()); const publicClient = createPublicClient({ chain: lineaSepolia, transport: http(rpcUrl), }); const walletClient = createWalletClient({ account: partyA, chain: lineaSepolia, transport: http(rpcUrl), }); const participants = [ { variableKey: 'partyAEthAddress', walletAddress: partyA.address, }, { variableKey: 'partyBEthAddress', walletAddress: partyB.address, }, ]; const deploymentValidation = await client.validateDeployment({ agreement, chainId, participants, }); console.log('Deployment preflight:', { variables: deploymentValidation.variables, participants: deploymentValidation.participants, observers: deploymentValidation.observers, contributors: deploymentValidation.contributors, warnings: deploymentValidation.warnings, }); type ProtocolInitValue = string | bigint | boolean | `0x${string}`; const deploymentInitValues = deploymentValidation.variables as Record; const deadline = computeDefaultDeadlineSeconds(); const deployPermit = await signDeployWithPermit({ walletClient, publicClient, chainId, agreement, deadline, permitOptions: { initValues: deploymentInitValues, }, }); console.log('Deploy permit signed but not submitted:', { signerAddress: deployPermit.signerAddress, deadline: deployPermit.deadline, signatureV: deployPermit.signature.v, signatureR: deployPermit.signature.r, signatureS: deployPermit.signature.s, }); ``` ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npx tsx quickstart.ts ``` The generated Party A wallet signs the deploy permit. The generated wallets are for this local test only; do not persist generated private keys or use this pattern for production custody. ## Completion state You have completed this quickstart when the script prints: * a healthy API response * an authenticated agreement list response * template validation output for the Simple Agreement * deployment preflight output * `signatureV`, `signatureR`, and `signatureS` from `signDeployWithPermit(...)` At that point your TypeScript integration has reached API auth, agreement validation, deployment preflight, chain/RPC context, and EIP-712 signing readiness. It has not deployed an agreement. ## Continue from here Continue from first-flight readiness to deployment, signed input submission, state reads, and input history. Reference constructor options, methods, signing helpers, diagnostics, path helpers, and exports. Model terms, variables, participants, states, inputs, issuers, and transitions. Learn the live deployment workflow after preflight and signing readiness are working. # TypeScript client reference Source: https://docs.shodai.network/sdks/typescript-client Reference the `@shodai-network/agreements-api-client` constructor, methods, signing helpers, diagnostics, path helpers, and exports. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Use [Quickstart with TypeScript SDK](/sdks/quickstart-with-typescript-sdk) for first setup. Use this page after connection to choose client methods, signing helpers, diagnostics, path helpers, and exports. MCP users who do not already have a wallet or signing service can also use this page to set up the TypeScript SDK with `viem` as a local testnet signing harness for typed data prepared by [Quickstart with MCP](/sdks/quickstart-with-mcp). Use literal SDK symbols exactly as exported by the package, including `ApiClient`, `AgreementsApiError`, `agreementsApiPaths`, and `API_BASE_PATH`. The package targets Node `>=18`. ## Install ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install @shodai-network/agreements-api-client viem ``` `@shodai-network/agreements-protocol-evm` is installed automatically as a package dependency. Install `viem` in your app when you use permit-signing helpers. The Agreements API client and the onchain protocol SDK are both published under the `@shodai-network` npm organization. The `agreements-api-playground` sample application uses `@shodai-network/agreements-api-client` for API calls and `@shodai-network/agreements-protocol-evm` for onchain agreement typing and signing support. ## Create a client ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { ApiClient } from '@shodai-network/agreements-api-client'; const client = new ApiClient({ environment: 'testnet', apiKey: process.env.API_KEY, }); const health = await client.getHealth(); ``` Constructor config: | Field | Use | | --------------- | ------------------------------------------------------------------------------------------------------------------------- | | `environment` | Named API environment. Supported values are `testnet` and `production`. | | `baseUrl` | Optional API host base URL override, without `/v0` appended. Use for local proxies, staging hosts, or custom deployments. | | `apiKey` | Optional `X-API-Key` value. Most API methods require it. | | `tokenProvider` | Optional bearer-token provider called before each request. Use it instead of `apiKey`. | | `headers` | Optional header record or header factory merged into each request. | | `fetch` | Optional fetch implementation. Defaults to `globalThis.fetch`. | If both `environment` and `baseUrl` are supplied, `baseUrl` wins. `testnet` resolves to `https://test-api.shodai.network`; `production` resolves to `https://api.shodai.network`. `apiKey` and `tokenProvider` are mutually exclusive. For a user-delegated provider in an installed Node.js client, follow [Connect an installed TypeScript client with delegated OAuth](/sdks/delegated-oauth-with-typescript). For key provisioning, scopes, entitlements, and `401`/`402`/`403` behavior, see [Authentication](/authentication). ## Wallet and RPC requirements The API key authenticates requests. Deploying agreements and submitting agreement inputs also require a wallet your integration controls because those workflows depend on EIP-712 signatures. | Requirement | Why it matters | | ------------------ | --------------------------------------------------------------------------------------------------- | | `walletClient` | Controls the signing account and produces EIP-712 signatures. A plain wallet address is not enough. | | `publicClient` | Reads target-chain context, factory or agreement contract state, and current permit nonces. | | Target chain/RPC | Must match the selected deployment chain or the deployed agreement's chain. | | Signer eligibility | For inputs, the signing wallet must be allowed by the authored input `issuer`. | ### Create a test-only wallet client For automated tests that only need EIP-712 signatures, create an ephemeral wallet with `viem`. This wallet does not need gas when it only signs permits. Do not use this pattern for production wallets, and do not commit generated private keys. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { createPublicClient, createWalletClient, http } from 'viem'; import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; import { lineaSepolia } from 'viem/chains'; const account = privateKeyToAccount(generatePrivateKey()); const walletClient = createWalletClient({ account, chain: lineaSepolia, transport: http(process.env.RPC_URL), }); const publicClient = createPublicClient({ chain: lineaSepolia, transport: http(process.env.RPC_URL), }); ``` The `testnet` API environment supports Linea Sepolia (`59141`), Ethereum Sepolia (`11155111`), and Base Sepolia (`84532`) for agreement deployments. The `production` API environment supports Linea Mainnet (`59144`) and Base Mainnet (`8453`). Choose the `chainId` explicitly for deployment preflight and deploy requests, then reuse the deployed agreement record's `chainId` when signing inputs. Use `account.address` in participant mappings when the test wallet needs to deploy an agreement or submit an input for a participant role. Before calling `deployAgreementWithPermit(...)` or `submitAgreementInputWithPermit(...)`, confirm that: 1. you have an API key for authenticated requests 2. you have a `walletClient` that can sign with the intended account 3. you have a `publicClient` connected to the target chain 4. the wallet chain matches the deployment or agreement chain 5. the signer is appropriate for the deployment or input issuer 6. you can regenerate signatures when nonce, deadline, payload, chain, agreement JSON, or values change ## Choose the right SDK surface | Use case | Prefer | | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | Normal API calls | `ApiClient` methods such as `validateTemplate`, `validateDeployment`, and `getAgreementState`. | | Deploy with an EIP-712 permit | `deployAgreementWithPermit(...)`. | | Submit a signed input | `submitAgreementInputWithPermit(...)`. | | Sign first, submit later, or customize request composition | `signDeployWithPermit(...)` or `signAgreementInputPermit(...)` plus `client.deployWithPermit(...)` or `client.submitAgreementInput(...)`. | | Debug HTTP status, headers, or raw body text | `client.exchangeJson(...)`. | | Compose a raw request path safely | `agreementsApiPaths`. | | Construct typed data manually without SDK helpers | [EIP-712 Signing Reference](/reference/eip-712-signing). | ## Main client methods The public API returns JSON envelopes for authenticated agreement routes. SDK methods that read one resource unwrap `data` and return the resource directly. List methods return the list envelope so your integration can read `data`, `pageInfo`, and `meta`. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const agreement = await client.getAgreement(agreementId); const page = await client.listAgreements({ limit: 25 }); console.log(page.data, page.pageInfo.nextCursor, page.meta.requestId); ``` | Method | Use | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `getOpenApiDocument()` | Fetch `GET /v0/openapi.json`. | | `getHealth()` | Check `GET /v0/health`. | | `createWebhook(body)` | Register a signed webhook endpoint. Returns the signing secret once. | | `listWebhooks()` | List webhook subscriptions for the API principal. | | `getWebhook(webhookId)` | Read one webhook subscription. | | `updateWebhook(webhookId, body)` | Update a webhook URL, filters, event types, or status. | | `deleteWebhook(webhookId)` | Disable a webhook subscription. | | `testWebhook(webhookId)` | Send a signed test payload to the subscription URL. | | `listAgreements(params)` | List paged agreement summaries visible to the API key. Supports `limit`, `cursor`, `sort`, `chainId`, `state`, and created/updated date filters. | | `getAgreement(agreementId)` | Read one agreement record. | | `getAgreementDocument(documentId)` | Read a generated agreement document through `GET /v0/agreements/documents/{documentId}`. | | `validateTemplate(agreement)` | Validate authored agreement JSON with `POST /v0/agreements/validate-template`. | | `validateDeployment(body)` | Preflight deployment context with `POST /v0/agreements/validate`. | | `deployWithPermit(body)` | Deploy with a prepared EIP-712 permit. | | `getAgreementState(agreementId)` | Read the current agreement state. | | `listAgreementInputs(agreementId, params)` | Read paged input history. Supports `limit`, `cursor`, `sort`, `userId`, `inputId`, `status`, and created/updated date filters. | | `submitAgreementInput(agreementId, body)` | Submit a signed input. | | `exchangeJson(method, path, body?)` | Inspect raw response metadata without throwing for HTTP errors. | | `request(method, path, body?, okStatus?)` | Low-level JSON request that returns the raw JSON body and throws on unexpected status. | List filters use `qs`-style bracket modifiers. For example, `{ createdAt: { gte: '...' }, sort: { createdAt: 'desc' } }` serializes as `createdAt[gte]=...&sort[createdAt]=desc`. Agreement lists support `chainId`, `state`, `createdAt`, `updatedAt`, `limit`, `cursor`, and one `sort` field from `createdAt`, `updatedAt`, or `displayName`. Input history supports `userId`, `inputId`, `status`, `createdAt`, `updatedAt`, `limit`, `cursor`, and one `sort` field from `createdAt` or `updatedAt`. Date filters support `gt`, `gte`, `lt`, and `lte`; `limit` must be between `1` and `100`. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const agreementsPage = await client.listAgreements({ chainId: 59141, state: 'AWAITING_PAYMENT', createdAt: { gte: '2026-05-01T00:00:00.000Z' }, sort: { createdAt: 'desc' }, limit: 25, }); const nextAgreementPage = agreementsPage.pageInfo.nextCursor ? await client.listAgreements({ cursor: agreementsPage.pageInfo.nextCursor, limit: 25 }) : null; const inputsPage = await client.listAgreementInputs('agreement-123', { userId: 'platform-user-id', status: 'FINALIZED', updatedAt: { lt: '2026-06-01T00:00:00.000Z' }, sort: { updatedAt: 'asc' }, limit: 25, }); ``` ## Receive webhook events Use webhooks when your integration should react to agreement activity or notification rules without polling agreement state. Webhook subscriptions can receive `agreement.transitioned` and `agreement.notification.triggered` events for agreement records associated with the current API principal. For signing, retries, filters, and delivery behavior, see [Receive webhooks](/webhooks/receive-webhooks). For event-family details, see [Agreement activity webhooks](/webhooks/agreement-activity-webhooks) and [Notification webhooks](/webhooks/notification-webhooks). ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const created = await client.createWebhook({ url: 'https://example.com/shodai/webhooks', eventTypes: ['agreement.transitioned', 'agreement.notification.triggered'], filters: { templateIds: ['did:template:service-retainer-v0-1'], }, }); console.log(created.id); console.log(created.secret); ``` Store `created.secret` immediately. It is returned only when the webhook is created. `listWebhooks`, `getWebhook`, `updateWebhook`, `deleteWebhook`, and `testWebhook` do not return the signing secret. To receive `agreement.notification.triggered`, include an `external_webhook` notification template when you deploy the agreement. The API scopes that template to the authenticated principal and deployed agreement. See [Notification webhooks](/webhooks/notification-webhooks) for supported triggers, temporal rules, recipients, interpolation, and payloads. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} await client.deployWithPermit({ agreement, displayName: 'Consulting Agreement', chainId: 59141, signer, deadline, signature, notificationTemplate: { rules: [ { id: 'deployment-follow-up', name: 'Deployment follow-up', trigger: { type: 'onTransition', inputs: ['__deploy'] }, recipients: ['@observers'], notification: { channel: 'external_webhook', subject: 'Agreement deployed', body: 'Agreement ${agreementId} is ready for review.', }, }, ], }, }); ``` Use `constructWebhookEvent(...)` from the webhook helper export to verify signed deliveries against the exact raw request body: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { constructWebhookEvent } from '@shodai-network/agreements-api-client/webhooks'; const event = constructWebhookEvent( rawBody, headers, process.env.SHODAI_WEBHOOK_SECRET!, ); ``` ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const subscriptions = await client.listWebhooks(); await client.testWebhook(subscriptions.data[0].id); ``` Use `deleteWebhook(webhookId)` to disable a subscription. The API returns the disabled subscription; it does not hard-delete the subscription record. ## Validate and deploy with helpers Use deployment preflight before signing when `initValues`, participant mappings, or observers affect the deployment request. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const chainId = 59141; const validation = await client.validateDeployment({ agreement, chainId, initValues, participants, observers, }); console.log(validation.variables); ``` Then use the high-level helper for the normal sign-and-submit path. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { deployAgreementWithPermit } from '@shodai-network/agreements-api-client'; const agreementRecord = await deployAgreementWithPermit({ client, walletClient, publicClient, chainId, agreement, displayName: 'Consulting Agreement', initValues: validation.variables, participants, observers, }); ``` Participant-derived values in `validation.variables` are the effective values the SDK signs for deployment. Keep the same `participants` array in `deployAgreementWithPermit(...)` so hosted agreement context records the participant mappings. When neither `docUri` nor `permitOptions.docUri` is supplied, `deployAgreementWithPermit(...)` generates a `documentId` and signs a `docUri` that points at `GET /v0/agreements/documents/{documentId}`. Pass `docUri` or `documentId` explicitly when your integration owns document addressing. ## Submit signed inputs with helpers ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { submitAgreementInputWithPermit } from '@shodai-network/agreements-api-client'; await submitAgreementInputWithPermit({ client, agreementId: agreementRecord.id, walletClient, publicClient, chainId: agreementRecord.chainId, agreementContractAddress, agreement, inputId: 'partyASignature', values, }); ``` `publicClient` must be connected to the target chain/RPC. Input submissions also require `chainId` from the deployed agreement record so the helper can fail before signing if the client is connected to the wrong chain. The signing helpers use `publicClient` to resolve the chain-specific `AgreementFactory` or agreement contract context and to read the current permit nonce before signing. ## Control deadlines and low-level signing ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { computeDefaultDeadlineSeconds, signAgreementInputPermit, signDeployWithPermit, } from '@shodai-network/agreements-api-client'; ``` `DEFAULT_PERMIT_DEADLINE_SECONDS` is `3600`. `computeDefaultDeadlineSeconds(offsetSeconds = 3600)` returns the current Unix time plus the offset. High-level `deployAgreementWithPermit(...)` and `submitAgreementInputWithPermit(...)` default the deadline. Low-level `signDeployWithPermit(...)` and `signAgreementInputPermit(...)` require an explicit deadline. Use low-level signing helpers when your application needs to sign first and submit later, inspect the signature, or compose the request body itself. Use [EIP-712 Signing Reference](/reference/eip-712-signing) only when you need to construct the typed data directly or debug helper behavior. ## Handle API errors The client throws `AgreementsApiError` when the response status does not match the expected success status. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { AgreementsApiError } from '@shodai-network/agreements-api-client'; try { await client.listAgreements(); } catch (error) { if (error instanceof AgreementsApiError) { console.error(error.status, error.errorPayload?.error.message ?? error.bodyText); } throw error; } ``` `AgreementsApiError` exposes `status`, `bodyText`, `parsedBody`, and `errorPayload`. `errorPayload.error.code` is stable for branching, and `errorPayload.error.requestId` is the value to share when you need support to trace a request. ## Use raw exchange for diagnostics Use `exchangeJson()` when you need response metadata or raw body text during debugging. It does not throw for HTTP error status codes. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const response = await client.exchangeJson('GET', '/v0/agreements'); console.log(response.status, response.ok, response.parsedBody); ``` `exchangeJson()` accepts `DELETE`, `GET`, `PATCH`, and `POST`. It returns `status`, `ok`, `headers`, `bodyText`, and `parsedBody`. ## Use path helpers when composing raw calls ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { agreementsApiPaths } from '@shodai-network/agreements-api-client'; const path = agreementsApiPaths.agreementState('agreement-123'); ``` Path helper names are `openapiJson`, `health`, `webhooks`, `webhook`, `webhookTest`, `agreements`, `agreementsValidate`, `agreementsValidateTemplate`, `agreementsDeployWithPermit`, `agreementDocument`, `agreement`, `agreementState`, `agreementInputs`, and `agreementInput`. ID path helpers encode IDs and use `API_BASE_PATH`, which is `/v0`. ## Root exports The package root exports these values, classes, and functions: * `ApiClient` * `AgreementsApiError` * `extractAgreementsApiErrorMessage` * `agreementsApiPaths` * `getExecutionInputIds` * `joinUrl` * `API_BASE_PATH` * `API_ENVIRONMENT_BASE_URLS` * `API_MAJOR_VERSION` * `DEFAULT_API_ENVIRONMENT` * `resolveApiBaseUrl` * `buildAgreementDocumentUri` * `createAgreementDocumentId` * `computeDefaultDeadlineSeconds` * `DEFAULT_PERMIT_DEADLINE_SECONDS` * `signDeployWithPermit` * `signAgreementInputPermit` * `deployAgreementWithPermit` * `submitAgreementInputWithPermit` The package root exports these TypeScript types: * `AgreementInputRecord` * `AgreementDocumentResponse` * `AgreementInputListParams` * `AgreementInputListSortField` * `AgreementListParams` * `AgreementListSortField` * `AgreementRecord` * `AgreementSummary` * `AgreementStateResponse` * `ApiResponse` * `DateFilter` * `DirectDeployAgreementWithPermitRequest` * `ErrorResponse` * `HealthResponse` * `ListResponse` * `NotificationRule` * `NotificationTemplate` * `PageInfo` * `ParticipantRecord` * `ApiClientConfig` * `AgreementsApiEnvironment` * `CreateWebhookRequest` * `CreateWebhookResponse` * `DirectParticipantRecord` * `PermitSignature` * `ProcessInputRequest` * `SortDirection` * `SortFilter` * `UpdateWebhookRequest` * `ValidateDirectAgreementRequest` * `ValidateDirectAgreementResponse` * `ValidateDirectAgreementTemplateResponse` * `WebhookEventType` * `WebhookFilters` * `WebhookSubscriptionEventType` * `WebhookSubscription` * `WebhookSubscriptionStatus` * `WebhookTestResponse` * `DeployWithPermitCallParams` * `SignDeployPermitParams` * `SignDeployPermitResult` * `SignInputPermitParams` * `SignInputPermitResult` * `SubmitInputCallParams` The `@shodai-network/agreements-api-client/client` export provides `ApiClient`. The `@shodai-network/agreements-api-client/webhooks` export provides webhook receiver helpers and types: * `constructWebhookEvent` * `verifyWebhookSignature` * `computeWebhookSignature` * `WebhookVerificationError` * `WebhookVerificationErrorCode` * `WebhookRawBody` * `WebhookHeaders` * `WebhookEventType` * `WebhookEventEnvelope` * `WebhookTestData` * `WebhookTestEvent` * `AgreementTransitionedWebhookData` * `AgreementTransitionedWebhookEvent` * `NotificationAttachmentStrategy` * `AgreementNotificationTriggeredWebhookData` * `AgreementNotificationTriggeredWebhookEvent` * `UnknownWebhookEvent` * `ShodaiWebhookEvent` * `VerifiedWebhookMetadata` * `ConstructWebhookEventOptions` * `WEBHOOK_ID_HEADER` * `WEBHOOK_TIMESTAMP_HEADER` * `WEBHOOK_SIGNATURE_HEADER` * `DEFAULT_WEBHOOK_TOLERANCE_SECONDS` * `WEBHOOK_API_VERSION` The Node-only `@shodai-network/agreements-api-client/oauth` export provides these delegated OAuth symbols used by the installed-client guide: * `OauthDelegatedSession` * `OauthDelegatedTokenSet` ## Related pages * [Quickstart with TypeScript SDK](/sdks/quickstart-with-typescript-sdk) * [Connect an installed TypeScript client with delegated OAuth](/sdks/delegated-oauth-with-typescript) * [Quickstart with MCP](/sdks/quickstart-with-mcp) * [Run an end-to-end agreement workflow](/examples/end-to-end-workflow) * [Authentication](/authentication) * [Validate Agreement Structure](/workflow/validate-agreement-structure) * [Deploy an Agreement](/workflow/deploy-an-agreement) * [Operate a Deployed Agreement](/workflow/operate-a-deployed-agreement) * [Receive webhooks](/webhooks/receive-webhooks) * [Agreement activity webhooks](/webhooks/agreement-activity-webhooks) * [Notification webhooks](/webhooks/notification-webhooks) * [Shodai Reference App](/examples/reference-app) * [EIP-712 Signing Reference](/reference/eip-712-signing) * [Errors and troubleshooting](/reference/errors-and-troubleshooting) # Contracts Source: https://docs.shodai.network/system-architecture/contracts Find current Agreements Protocol EVM contract addresses and verified source links. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Use this page when you need to inspect deployed onchain execution contracts directly, verify source code, or confirm a factory address used for low-level signing. The `AgreementFactory` address is the `verifyingContract` for deploy permit signatures. Each deployed agreement is an `AgreementEngine` clone created by the factory. Input permit signatures use the deployed agreement address as the `verifyingContract`. The TypeScript SDK resolves factory addresses from its protocol deployment registry for the selected `chainId`. The table below mirrors the current registry deployments. ## Current deployments | Network | Chain ID | Contract | Address | Verified source | | ---------------- | ---------- | -------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | Base Mainnet | `8453` | `AgreementFactory` | `0x76dAA59C02d902e7063E6328D2E64ACee6CC121e` | [Verified source](https://basescan.org/address/0x76dAA59C02d902e7063E6328D2E64ACee6CC121e#code) | | Base Mainnet | `8453` | `AgreementEngine` implementation | `0x1c2961AC5e2fBE47b3C0654d7dF543dB04F031A6` | [Verified source](https://basescan.org/address/0x1c2961AC5e2fBE47b3C0654d7dF543dB04F031A6#code) | | Linea Sepolia | `59141` | `AgreementFactory` | `0x26Ff3AdEC23fC5778f190371B1CcCadDa74e26c8` | [Verified source](https://sepolia.lineascan.build/address/0x26Ff3AdEC23fC5778f190371B1CcCadDa74e26c8#code) | | Linea Sepolia | `59141` | `AgreementEngine` implementation | `0x1F0f9c889E6c762D8D5669c65a1A8fDFbEe38664` | [Verified source](https://sepolia.lineascan.build/address/0x1F0f9c889E6c762D8D5669c65a1A8fDFbEe38664#code) | | Linea Mainnet | `59144` | `AgreementFactory` | `0xB772Ea12546fd7153Bf1F5ED7266B8faB0dAD6C9` | [Verified source](https://lineascan.build/address/0xB772Ea12546fd7153Bf1F5ED7266B8faB0dAD6C9#code) | | Linea Mainnet | `59144` | `AgreementEngine` implementation | `0x9263A3a927939Aa76bE7bFa1850A1ef50454e122` | [Verified source](https://lineascan.build/address/0x9263A3a927939Aa76bE7bFa1850A1ef50454e122#code) | | Base Sepolia | `84532` | `AgreementFactory` | `0x76dAA59C02d902e7063E6328D2E64ACee6CC121e` | [Verified source](https://sepolia.basescan.org/address/0x76dAA59C02d902e7063E6328D2E64ACee6CC121e#code) | | Base Sepolia | `84532` | `AgreementEngine` implementation | `0x1c2961AC5e2fBE47b3C0654d7dF543dB04F031A6` | [Verified source](https://sepolia.basescan.org/address/0x1c2961AC5e2fBE47b3C0654d7dF543dB04F031A6#code) | | Ethereum Sepolia | `11155111` | `AgreementFactory` | `0x76dAA59C02d902e7063E6328D2E64ACee6CC121e` | [Verified source](https://sepolia.etherscan.io/address/0x76dAA59C02d902e7063E6328D2E64ACee6CC121e#code) | | Ethereum Sepolia | `11155111` | `AgreementEngine` implementation | `0x1c2961AC5e2fBE47b3C0654d7dF543dB04F031A6` | [Verified source](https://sepolia.etherscan.io/address/0x1c2961AC5e2fBE47b3C0654d7dF543dB04F031A6#code) | The verified source links open each network explorer's code tab. ## Contract roles | Contract | Role | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `AgreementFactory` | Deploys new agreement instances and verifies deploy permit signatures. Use this address as the factory address for direct deploy signing. | | `AgreementEngine` implementation | Shared implementation for agreement clone instances. Each deployed agreement clone stores its own document hash, state, inputs, transitions, verifier registrations, initialization values, and optional actions. | For low-level typed data, see [EIP-712 Signing Reference](/reference/eip-712-signing). For the execution model, see [Onchain execution engine](/system-architecture/on-chain). # Agreement data standard Source: https://docs.shodai.network/system-architecture/data-standard Understand how the data standard defines agreement definitions: human-readable content, variables, participants, inputs, states, transitions, and execution history. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). The agreement data standard is the common language of Agreements Protocol. It defines the JSON structure used to produce an agreement definition: the shared object that applications render, agents inspect, and execution engines enforce. The standard is maintained in [CNSLabs/agreements-standard](https://github.com/CNSLabs/agreements-standard). Complete examples are available in [Simple Agreement](/examples/simple) and [Complex Agreement](/examples/complex). ## What the standard describes Use agreement JSON to produce an agreement definition that describes: * human-readable agreement content * participants and participant-backed variables * allowed inputs and authorized issuers * lifecycle states and state transitions * outcomes and optional actions * optional contract references used by runtime validation or actions The important property is that the agreement definition is data, not custom per-agreement application code. The same definition can be validated by the Agreements API, prepared by SDK helpers, and executed by the [onchain execution engine](/system-architecture/on-chain). Because the data standard is separate from any single runtime, the agreement-data layer is where the protocol semantics live. The EVM engine is the first concrete execution engine for those semantics. ## Agreement sections Select a section to see the part of the agreement definition it controls. `metadata` identifies the agreement definition and gives downstream tools stable naming, versioning, and template context. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "metadata": { "id": "did:example:mou-v1", "templateId": "did:template:mou-v1", "version": "1.0.0", "createdAt": "2024-03-20T12:00:00Z", "name": "Grant With Feedback", "author": "Agreements Protocol", "description": "A human-readable agreement with executable lifecycle rules." } } ``` `variables` define the reusable facts, participant roles, types, and validation hints the agreement depends on. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "variables": { "partyAName": { "type": "string", "name": "Party A Name", "description": "Legal name of the first party", "validation": { "required": true, "minLength": 1 } }, "granteeAddress": { "type": "address", "subtype": "participant", "name": "Grantee Wallet" } } } ``` `contracts` can attach runtime contract references and chain context that actions or validation logic need. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "contracts": { "workToken": { "description": "The Work Token", "address": "0x12be78ca652191616f49420dfa28214bafe9326c", "chainId": "59141", "abi": "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\"}]}]" } } } ``` `content` is the human-readable agreement text. It can reference variables so rendered prose and executable data stay aligned. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "content": { "type": "md", "data": "# SIMPLE GRANT AGREEMENT\n\n**BETWEEN GRANTOR:**\n\n${variables.grantorName} (Grantor)\n\n${variables.granteeName} (Grantee)\n\nThe parties agree to the grant scope, duration, and payment terms." } } ``` `execution` defines the execution path: the initial state, allowed inputs, validation conditions, transitions, and optional actions. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "execution": { "states": { "AWAITING_TEMPLATE_VARIABLES": { "name": "Awaiting Template Variables" }, "AWAITING_RECIPIENT_SIGNATURE": { "name": "Pending Recipient Signature" } }, "initialize": { "initialState": "AWAITING_TEMPLATE_VARIABLES" }, "inputs": { "grantorData": { "type": "VerifiedCredentialEIP712", "displayName": "Template Variable Submission" } }, "transitions": [ { "from": "AWAITING_TEMPLATE_VARIABLES", "to": "AWAITING_RECIPIENT_SIGNATURE", "conditions": [{ "type": "isValid", "input": "grantorData" }] } ] } } ``` ## Inputs Inputs define the valid interactions that can move an agreement forward. An input definition specifies: * the input type * the expected data shape * the authorized issuer * optional display metadata * how submitted data maps into agreement variables or transition conditions At runtime, an input submission must match the authored input definition and be submitted by an authorized issuer. In the EVM implementation, this authorization is expressed through EIP-712 signed input data. The current examples use `VerifiedCredentialEIP712` inputs, which are supported by the current EVM execution engine. Use only input types and subtypes documented for that engine. ## How the standard moves through the system 1. You author agreement JSON as an agreement definition. 2. The Agreements API validates the agreement definition before deployment context is added. 3. [Deployment supplies live `initValues`, participant mappings, observers, and signed authorization.](/workflow/deploy-an-agreement) 4. SDK helpers convert the authored definition and deployment values into engine-compatible payloads. 5. The onchain execution engine stores and enforces the execution path for that deployed agreement instance. For authoring guidance, see [Author Agreement JSON](/workflow/author-agreement-json) and [Validate Agreement Structure](/workflow/validate-agreement-structure). # Onchain execution engine Source: https://docs.shodai.network/system-architecture/on-chain Understand how the EVM execution engine deploys agreement definitions and enforces valid inputs, issuers, states, transitions, and history. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). The onchain execution engine is the first concrete runtime for Agreements Protocol. It is implemented in [CNSLabs/agreements-protocol-evm](https://github.com/CNSLabs/agreements-protocol-evm), including Solidity contracts and the lower-level `@shodai-network/agreements-protocol-evm` SDK. The engine interprets agreement definitions produced from the [agreement data standard](/system-architecture/data-standard) and gives them a verifiable onchain execution surface. The current onchain implementation is EVM-based, and each API environment can expose more than one agreement deployment chain. The `testnet` API environment uses the testnet chains: Linea Sepolia, Ethereum Sepolia, and Base Sepolia. The `production` API environment uses the production chains: Linea Mainnet and Base Mainnet. ## Execution model Each deployed agreement definition becomes an isolated onchain execution instance. The `AgreementFactory` deploys `AgreementEngine` instances, and each instance stores the agreement-specific inputs, verifiers, transitions, initialization values, and optional actions. Agreements are deployed through an `AgreementFactory`. Each deployed agreement has an associated engine that tracks its state, validates submitted inputs, and applies the authored transition rules. The agreement's deployed behavior is derived from the agreement definition and associated deployment data. Once deployed, the core agreement definition is fixed, which makes the agreement reliable as a shared operational source of truth. For signing details, typed data, and current factory addresses, see the [EIP-712 Signing Reference](/reference/eip-712-signing). ## What the engine enforces The engine enforces the agreement lifecycle: 1. A participant submits an input. 2. The engine validates and verifies the submitted payload. 3. The engine checks whether the input is allowed from the current state. 4. The engine updates state when the transition is valid. 5. Optional actions run as part of the configured transition. 6. The result is emitted through onchain state and events. This gives each agreement deterministic execution: the same agreement definition and same valid inputs lead to the same state transition and result. ## Agreement instances and execution history A deployed agreement instance produces a sequence of events, state transitions, submitted inputs, and final outcomes. That history becomes the shared source of truth for the agreement. Participants do not need to reconcile independent application databases to understand what happened. They can read the agreement instance, inspect input history, and verify the state that follows from accepted inputs. ## Direct engine usage Use the direct onchain path when your integration already works with wallets, RPC clients, and contract-level execution. The onchain repository contains: | Area | Purpose | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `contracts/` | Solidity contracts, tests, deployment scripts, and deployment artifacts. | | `sdk/` | TypeScript SDK package for `AgreementFactory`, `AgreementEngine`, ABIs, deployments, and payload helpers. Published as `@shodai-network/agreements-protocol-evm`. | | `agreements/` | Sample agreement artifacts used for execution-engine tests and examples. | For current factory and implementation addresses, see [Contracts](/system-architecture/contracts). For most application integrations, start with the [API client](/sdks/typescript-client) instead. It uses the onchain SDK where needed but gives you higher-level validation, signing, deployment, state, and input-history methods. See [Agreements API](/system-architecture/putting-it-together). # Architecture overview Source: https://docs.shodai.network/system-architecture/overview Orient around agreement definitions, deployed agreement instances, onchain execution, SDKs, and supporting API layers. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). ## Architecture at a glance Shodai turns agreements into living, verifiable infrastructure: programmable commitments with explicit state, signatures, transitions, and history. The agreement definition is the center of the system. It is produced from the data standard, rendered as human-readable agreement content, and deployed to the onchain engine to create a deployed agreement instance. Outside the protocol core, SDKs, APIs, and applications use definitions and instances to create user experiences around agreement creation, submission, inspection, and monitoring. | Layer | Role | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | Data standard | Defines the common agreement language and schema semantics. | | Agreement definition | Describes prose, variables, participant-backed address variables, inputs, states, transitions, actions, and references. | | Deployed agreement instance | Records live initialization values, participant address mappings, current state, accepted inputs, emitted events, and transition history. | | Onchain execution engine | Deploys agreement instances and enforces valid inputs, issuers, states, transitions, and recorded history. | | SDK and client libraries | Help applications validate, prepare, sign, deploy, submit inputs, and read agreement state. | | Supporting API / application layer | Creates, deploys, indexes, monitors, notifies, and integrates agreements into products. | | Pluggable modules | Add specialized capabilities such as payment integrations, roles, attestations, dispute flows, and domain-specific execution. | ## Native vs composed behavior Shodai's core engine focuses on agreement structure, authorization, valid inputs, state progression, and verifiable history. Domain-specific behavior such as escrow, payment execution, dispute resolution, compliance checks, notifications, or private-data verification can be composed through modular actions, external modules, or application-layer integrations. This keeps the core agreement model generic while allowing richer behavior to be added around it. ## Data standard in practice The data standard gives every agreement the same shape: metadata, typed variables, human-readable content, expected inputs, lifecycle states, and transitions. In a Memorandum of Understanding template, the diagram below is derived from the `execution` section of the agreement definition. The following visual example shows an MOU agreement definition and a derived state machine. The execution path is: PENDING\_PARTY\_A\_SIGNATURE --partyAData--> PENDING\_PARTY\_B\_SIGNATURE --partyBData--> PENDING\_ACCEPTANCE; PENDING\_ACCEPTANCE --accepted--> ACCEPTED; PENDING\_ACCEPTANCE --rejected--> REJECTED. ## Definition vs instance An agreement definition describes the agreement's terms and valid execution path. It is authored from the agreement data standard and can be rendered, validated, inspected, and prepared for deployment. A deployed agreement instance is the runtime form of that definition. It has live initialization values, participant address mappings, current state, accepted inputs, emitted events, and execution history. ## How the parts fit together 1. Author an agreement definition from the [agreement data standard](/system-architecture/data-standard). 2. Render the same definition for human review and application workflows. 3. Use SDKs or the Agreements API to validate, prepare, sign, and deploy the agreement. 4. Deploy the definition to the [onchain execution engine](/system-architecture/on-chain), creating a deployed agreement instance. 5. Participants submit defined inputs to the deployed instance as the agreement progresses. 6. The engine validates issuer constraints, schemas, states, and transitions, then records state and history. 7. Applications can read deployed instance state directly or use the [Agreements API](/system-architecture/putting-it-together) for indexing, monitoring, notifications, and product integrations. ## API-assisted and direct onchain operation The documented happy path uses the Shodai API and SDK to simplify validation, signing, deployment, indexing, and application integration. The onchain execution layer is the verifiable substrate. Developers who need stronger trust-minimization or resilience can inspect the relevant contracts, typed data, chain configuration, addresses, and events directly in [`CNSLabs/agreements-protocol-evm`](https://github.com/CNSLabs/agreements-protocol-evm). The API-assisted path and the direct onchain path are complementary: * the API helps applications deploy and operate agreements more easily * the onchain engine provides the verifiable agreement runtime * EIP-712 typed data connects user authorization to onchain execution ## Protocol boundary The protocol is defined by the agreement data standard and agreement definitions. The EVM engine is the first concrete execution engine for those semantics. The API does not define what an agreement means. It helps applications work with agreement definitions and deployed agreement instances by creating, deploying, indexing, monitoring, notifying, and integrating agreements into products. | Boundary | Includes | | ---------------------- | --------------------------------------------------------------------------------------------------------- | | Protocol semantics | Agreement data standard and agreement definition. | | Runtime implementation | Onchain execution engine that interprets agreement definitions and enforces deployed agreement instances. | | Developer tooling | SDKs and client libraries for validation, signing, deployment, input submission, and reads. | | Application layer | API services, indexing, monitoring, notifications, integrations, and user experiences. | ## What becomes verifiable Agreements Protocol makes agreement progression constrained, inspectable, and verifiable. Once an agreement definition is deployed, participants and agents can inspect the current state, accepted inputs, and transition history without relying only on an application, API, or private database. | Verifiable property | What it means | | ------------------------- | ---------------------------------------------------------------------------------- | | Constrained inputs | Only defined inputs can move the agreement. | | Authorized issuers | Inputs can be bound to specific parties or addresses. | | Deterministic progression | Valid inputs produce defined state transitions. | | Auditable sequence | Accepted inputs and state changes are preserved. | | Inspectable state | Current state and history can be read by applications, parties, and agents. | | Reduced ambiguity | The agreement's operational path is explicit, not hidden in prose or private code. | ## What this enables As a developer, you get a reusable way to model agreements, deterministic multi-party execution, verifiable outcomes across participants, and a consistent interface across different agreement types. That matters because the agreement definition becomes the shared object that humans read, applications render, agents inspect, and execution engines enforce. ## Choose your path Understand the JSON structure that describes agreement prose, variables, inputs, states, transitions, and valid execution paths. See how agreement definitions become verifiable onchain agreement instances. Understand why the API is the product integration layer around agreement creation, deployment, monitoring, and participant workflows. Learn which repositories own the data standard, EVM execution engine, and TypeScript API client. ## Summary The data standard defines the common language. The agreement definition describes the prose, variables, inputs, states, transitions, and valid execution path. A deployed agreement instance records current state, accepted inputs, events, and transition history. SDKs, APIs, and application workflows make it easier to create, deploy, monitor, and integrate agreements into real products. The result is a protocol for agreement progression: not just what parties accepted, but how the agreement can move, who can move it, and how that movement can be inspected. # Agreements API Source: https://docs.shodai.network/system-architecture/putting-it-together Understand why the Agreements API is the product integration layer for agreement creation, deployment, monitoring, and participant workflows. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). The Agreements API is the product integration layer around Agreements Protocol. It gives applications a stable way to turn agreement definitions into deployed, monitored, participant-facing workflows without making every product own contract orchestration, indexing, notification routing, and lifecycle state. The API does not define what an agreement means. Agreement semantics come from the [agreement data standard](/system-architecture/data-standard) and from the execution engine that interprets those definitions. The API makes those protocol objects usable inside real products. Use the API layer when you want agreement correctness and verifiable execution to show up as ordinary application capabilities: create agreements, attach participants, deploy with signed authorization, read state, submit participant inputs, inspect history, and trigger downstream product behavior. ## Why the API matters Agreements Protocol separates agreement semantics from application delivery. The data standard gives every agreement a uniform language. The onchain execution engine gives deployed agreements constrained, verifiable state transitions. The API connects those layers to the systems where users actually create, accept, operate, and monitor agreements. That matters because most products need more than a contract call. They need draft records, participant context, observers, validation feedback, deployment preflight, state reads, input history, authorization, and notifications. The API provides that supporting layer while keeping the protocol semantics anchored in the agreement definition and execution engine. ## What the API adds | Capability | Why it matters | | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Agreement creation and records | Applications can manage draft and deployed agreement records without treating the chain as their only product database. | | Structural validation | Products can catch missing participant roles, unexpected state IDs, input IDs, and authoring warnings before deployment context is added. | | Deployment preflight | Teams can review the effective deployment values, participant mappings, observers, contributors, and warnings before any permit is signed. | | Signed deployment and input submission | The API works with [SDK signing helpers](/sdks/typescript-client#control-deadlines-and-low-level-signing) so products can preserve wallet authorization while avoiding low-level contract orchestration in every integration. | | State and history reads | Applications can show current state, accepted inputs, input status, and agreement progression from one integration surface. | | Participant and observer context | Products can attach participant wallet mappings and observer email context around deployed agreement instances. | | Monitoring and notifications | Downstream systems can react to agreement lifecycle movement instead of polling raw execution state everywhere. | ## The value for product teams The API lets product teams build agreement experiences around a common execution model instead of rebuilding agreement infrastructure for every workflow. * A marketplace can use the same agreement model for milestone acceptance, rejection, resubmission, and completion. * A service platform can expose participant actions while preserving state transitions that are constrained by the deployed agreement. * An operations product can monitor agreement status, history, observers, and notifications without becoming the source of truth for the agreement lifecycle. * An API integration can use typed client methods and raw API references without depending on private implementation details of the execution engine. The result is a cleaner boundary: products own user experience, identity context, notifications, and business operations; the agreement definition and execution engine own the explicit agreement path. ## How it preserves trust The API is designed to support the trust guarantees of the protocol, not replace them. | Trust property | How the API supports it | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | Completeness before execution | Validation and preflight make agreement structure, participant roles, deployment values, and warnings visible before signing. | | Clear participant context | Participant mappings connect agreement roles to wallet addresses, while observers remain separate application context. | | Deterministic progression | Signed inputs are submitted against the deployed agreement instance, where the execution engine enforces allowed issuers, states, and transitions. | | Auditable history | State reads and input history expose the lifecycle that applications need to display, reconcile, and monitor. | | Substrate independence | Products integrate through agreement definitions and API workflows while the protocol semantics remain separate from a particular application database. | ## Where it sits in the system | Layer | Responsibility | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Agreement data standard | Defines the common language for agreement prose, variables, participants, inputs, states, transitions, and actions. | | Onchain execution engine | Interprets deployed agreement definitions and enforces valid inputs, issuers, states, transitions, and history. | | TypeScript client and signing helpers | Give application code typed methods and EIP-712 helpers for deployment and participant inputs. | | Agreements API | Provides hosted records, validation, preflight, deployment coordination, state reads, input history, participant context, and integration hooks. | | Products and API integrations | Build user experiences, operational processes, dashboards, and domain-specific workflows around agreement instances. | This keeps the core design minimal and composable. Agreement correctness lives in the definition and execution path. The API adds application convenience, workflow coordination, and legibility around that core. ## When to use the API Use the API when your integration needs any of the surrounding product behavior that most agreement workflows require: * hosted agreement records and queries * draft-to-deployment workflows * validation and deployment preflight * participant and observer context * signed deployment or signed participant inputs through SDK helpers * current state, input history, and lifecycle monitoring * notifications or application integrations triggered by agreement movement Use the direct onchain path when your system deliberately wants to own wallet orchestration, RPC access, contract interaction, indexing, and product workflow state itself. For most application integrations, the API is the more practical surface because it packages the repeated work around the same verifiable agreement core. ## What to read next Confirm API access, validate an example, preflight deployment, and sign a deploy permit without deploying. Understand the typed client methods, signing helpers, diagnostics, and path helpers. See the deployment workflow when you are ready to implement it. Read current state, submit signed inputs, and inspect agreement history. # Repositories Source: https://docs.shodai.network/system-architecture/repositories Understand which repositories own the agreement data standard, EVM execution engine, and TypeScript API client. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). The protocol source and its supporting developer stack span a small set of repositories. Use this page to understand where the agreement data standard, EVM execution engine, and TypeScript API client live. ## Repository map | Repository | Owns | Use it when | | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | [CNSLabs/agreements-standard](https://github.com/CNSLabs/agreements-standard) | The agreement data standard, source diagrams, and examples that describe protocol semantics. | You are working on the canonical agreement JSON shape, agreement examples, or standard-level diagrams. | | [CNSLabs/agreements-protocol-evm](https://github.com/CNSLabs/agreements-protocol-evm) | The EVM/onchain execution engine, Solidity contracts, execution tests, deployment artifacts, and the SDK in `agreements-protocol-evm/sdk`. The SDK is published as `@shodai-network/agreements-protocol-evm`. | You are working on `AgreementFactory`, `AgreementEngine`, input payload encoding, contract deployments, or direct execution-engine SDK behavior. | | [CNSLabs/agreements-api-sdk](https://github.com/CNSLabs/agreements-api-sdk) | The published Agreements API TypeScript client package, `@shodai-network/agreements-api-client`, and the `agreements-api-playground` sample application. | You are working on typed API methods, signing helpers, path helpers, diagnostics, package publishing, or the sample API playground. | ## How the repositories fit together The data standard defines protocol semantics and the agreement definition. The EVM repository provides the first concrete execution engine. The API SDK makes the hosted API usable from TypeScript application code. The API remains an application layer around the protocol, not part of the protocol semantics. | System layer | Primary repository | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | Definition and protocol semantics | [CNSLabs/agreements-standard](https://github.com/CNSLabs/agreements-standard) | | Onchain execution engine | [CNSLabs/agreements-protocol-evm](https://github.com/CNSLabs/agreements-protocol-evm) | | API client integration | [CNSLabs/agreements-api-sdk](https://github.com/CNSLabs/agreements-api-sdk) | | Supporting API / application layer | Hosted API and product services. Use [Agreements API](/system-architecture/putting-it-together) and the API Reference group in the sidebar. | The browser playground at `https://developers.shodai.network/api-playground` is powered by both npm packages: `@shodai-network/agreements-api-client` for API calls and `@shodai-network/agreements-protocol-evm` for onchain agreement interactions. ## Related pages * [Architecture](/system-architecture/overview) * [Agreement data standard](/system-architecture/data-standard) * [Onchain execution engine](/system-architecture/on-chain) * [Agreements API](/system-architecture/putting-it-together) * [Contracts](/system-architecture/contracts) # Agreement activity webhooks Source: https://docs.shodai.network/webhooks/agreement-activity-webhooks Use agreement.transitioned webhook events to mirror agreement lifecycle changes and reconcile current agreement state through the Agreements API. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Agreement activity webhooks are `agreement.transitioned` events. Use them when your backend needs to react to agreement lifecycle changes without polling agreement state on a timer. Treat each activity webhook as a compact signal: verify it, store and dedupe it, acknowledge it, and then read current agreement data from the Agreements API before updating your local mirror. ## Before you start Create a webhook subscription that includes `agreement.transitioned`: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const webhook = await client.createWebhook({ url: 'https://example.com/shodai/webhooks', eventTypes: ['agreement.transitioned'], filters: { templateIds: ['did:template:service-retainer-v0-1'], }, }); ``` For signing, retries, test events, URL requirements, and shared filter semantics, see [Receive webhooks](/webhooks/receive-webhooks). ## When activity events are sent Shodai sends `agreement.transitioned` for API-managed agreements owned by the Shodai account when: * the chain projection worker reconciles an agreement deployment into its initial state * a mined input changes the agreement state Deploy transition events are produced from finalized chain projection, not by the deployment response. The deployment request returns after the transaction receipt is mined, at roughly one confirmation, without waiting for webhook finality. Projection emits the canonical transition only after the deployment block reaches the shared finality pin at `head - 20`. Deploy transitions use `fromState: ""`, `inputId: "__deploy"`, and `sequence: 0`. Those are payload values, not filter values: to subscribe to deploy transitions only, filter with `inputIds: ["__deploy"]` — a blank filter value such as `fromStates: [""]` is rejected with 400. Input transition events are sent only when the mined input changes state. If an input is accepted but the agreement remains in the same state, no `agreement.transitioned` event is emitted for that input. ## Payload shape An activity event includes compact transition data: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "id": "effect_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "type": "agreement.transitioned", "apiVersion": "2026-06-01", "createdAt": "2026-06-02T18:00:00.000Z", "data": { "agreementId": "agr_123", "agreementName": "Advisory Retainer", "templateId": "did:template:service-retainer-v0-1", "fromState": "AWAITING_PAYMENT", "toState": "WORK_IN_PROGRESS", "inputId": "submitInitialPaymentProof", "sequence": 3 } } ``` | Field | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------- | | `agreementId` | Hosted agreement record ID. | | `agreementName` | Optional human-readable agreement display name. | | `templateId` | Agreement template ID from the agreement JSON metadata. | | `fromState` | Previous lifecycle state. Deploy transitions use an empty string. | | `toState` | New lifecycle state. | | `inputId` | Input that caused the transition. Deploy transitions use `__deploy`. | | `sequence` | Canonical per-agreement order. Deploy uses `0`; accepted inputs use their 1-based canonical position, so gaps are normal. | The activity payload does not include the full agreement record, variables, participants, observers, or full input history. ## Reconcile after receipt After verification and dedupe, read the current agreement data before updating local state: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { constructWebhookEvent } from '@shodai-network/agreements-api-client/webhooks'; const event = constructWebhookEvent(rawBody, headers, process.env.SHODAI_WEBHOOK_SECRET!); await storeWebhookEvent(event); if (event.type === 'agreement.transitioned') { const agreementId = event.data.agreementId; const agreement = await client.getAgreement(agreementId); const state = await client.getAgreementState(agreementId); const inputs = await client.listAgreementInputs(agreementId, { limit: 25 }); await updateLocalAgreementMirror({ agreement, state, inputs, lastWebhookEventId: event.id, }); } ``` Use `GET /v0/agreements/{id}` for the hosted agreement record, `GET /v0/agreements/{id}/state` for current lifecycle state, and `GET /v0/agreements/{id}/inputs` for input history. This pattern keeps your integration resilient when events arrive more than once, arrive after your own write response, or do not contain enough data to update your local model directly. ## Filter activity events Activity webhooks support these filters: | Filter | Matches | | -------------- | ------------------ | | `agreementIds` | `data.agreementId` | | `templateIds` | `data.templateId` | | `inputIds` | `data.inputId` | | `fromStates` | `data.fromState` | | `toStates` | `data.toState` | Filter values are exact string matches. Multiple values inside one filter field act like OR. Different filter fields combine like AND. Use `inputIds: ["__deploy"]` to receive only deploy activity events for matching agreements and templates. ## Idempotency expectations Webhook delivery is at-least-once. Your receiver should: * store each event by `id` * treat a repeated `id` as already received * return `2xx` after durable receipt * process reconciliation asynchronously when possible * make local mirror updates idempotent Activity events are compact and may arrive after your backend has already observed the same state through a write response or a manual refresh. Retries can also reorder arrival. For one agreement, apply events by `sequence`, ignore a sequence lower than the highest already applied, and use the current API state as the source of truth. Events emitted before `sequence` was introduced may omit it. ## Related pages * [Receive webhooks](/webhooks/receive-webhooks) * [Notification webhooks](/webhooks/notification-webhooks) * [Operate a Deployed Agreement](/workflow/operate-a-deployed-agreement) * [TypeScript client](/sdks/typescript-client) # Notification webhooks Source: https://docs.shodai.network/webhooks/notification-webhooks Use agreement.notification.triggered webhook events when Shodai should evaluate agreement notification rules and call your backend with the resolved notification content. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Notification webhooks are `agreement.notification.triggered` events. They use the same webhook subscription, signing, delivery, retry, and filtering system as agreement activity webhooks, but they are created by the notification rules attached to an agreement. Use notification webhooks when Shodai should evaluate agreement notification rules while your backend owns the final side effect, such as sending email through your own SES account. ## How notification webhooks work There are two required setup steps: 1. Create a webhook subscription that includes `agreement.notification.triggered`. 2. Deploy an agreement with an agreement-scoped `notificationTemplate` whose rules use `notification.channel: "external_webhook"`. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const webhook = await client.createWebhook({ url: 'https://example.com/shodai/webhooks', eventTypes: ['agreement.transitioned', 'agreement.notification.triggered'], }); ``` A subscription alone does not create notification events. Shodai sends `agreement.notification.triggered` only after an `external_webhook` notification rule resolves at least one recipient. ## Attach notification rules at deploy Pass `notificationTemplate` to `deployWithPermit`: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} await client.deployWithPermit({ agreement, displayName: 'Consulting Agreement', chainId: 59141, signer, deadline, signature, notificationTemplate: { rules: [ { id: 'deployment-follow-up', name: 'Deployment follow-up', trigger: { type: 'onTransition', inputs: ['__deploy'] }, recipients: ['@observers'], notification: { channel: 'external_webhook', subject: 'Agreement deployed', body: 'Agreement ${agreementId} is ready for review.', }, }, ], }, }); ``` After a successful deploy, the External API scopes the template to the authenticated Shodai account, deployed agreement ID, and agreement template ID. It does not replay a deploy transition. The deploy request returns after the transaction receipt is mined, while the returned chain attribution is still provisional. Shodai evaluates `__deploy` notification rules later, from the canonical deploy transition produced by finalized chain projection. Projection uses the shared finality pin at `head - 20`, so this notification does not arrive synchronously with the deploy response. ## onTransition rules `onTransition` rules evaluate when an agreement transition event reaches the notification service. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const notificationTemplate = { rules: [ { id: 'payment-received', name: 'Payment received', trigger: { type: 'onTransition', from: ['AWAITING_PAYMENT'], to: ['WORK_IN_PROGRESS'], inputs: ['submitInitialPaymentProof'], }, recipients: ['clientWalletAddress', '@observers'], notification: { channel: 'external_webhook', subject: 'Payment received for ${agreementName}', title: 'Work can begin', body: '${agreementName} moved to ${toState}.', ctaLabel: 'View agreement', }, }, ], }; ``` For `onTransition` triggers: * `from`, `to`, and `inputs` are optional arrays * omitted arrays act as wildcards * `inputs: ["__deploy"]` matches the finalized deploy transition when chain projection emits it * `fromState`, `toState`, and `input` are available for interpolation * the webhook payload includes a `transition` block ## Temporal rules Temporal rules evaluate while an agreement is in one of the configured states. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const notificationTemplate = { rules: [ { id: 'invoice-due-soon', name: 'Invoice due soon', trigger: { type: 'temporal', states: ['AWAITING_PAYMENT'], condition: { type: 'deadlineApproaching', variable: 'paymentDueAt', threshold: { value: 2, unit: 'days' }, }, fireOnce: true, }, recipients: ['customerWalletAddress'], notification: { channel: 'external_webhook', subject: 'Payment due soon for ${agreementName}', body: 'Payment is due soon for agreement ${agreementId}.', }, constraints: { maxOccurrences: 1, cooldown: { value: 1, unit: 'days' }, }, }, ], }; ``` Temporal triggers support: | Field | Behavior | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `states` | States where the temporal rule is active. | | `condition.type: "deadlineApproaching"` | Fires when a date variable is within the threshold and still in the future. | | `condition.type: "stateAge"` | Fires when time in the current state reaches the threshold. | | `condition.type: "elapsedSinceVariable"` | Fires when elapsed time since a date variable reaches the threshold. | | `threshold.unit` | `seconds`, `minutes`, `hours`, `days`, or `weeks`. | | `fireOnce` | Defaults to `true`; when true, fires once per state visit. | | `constraints.maxOccurrences` | Caps temporal firings for the rule and agreement. One firing counts once, regardless of recipient fan-out. | | `constraints.cooldown` | Requires a minimum time between temporal sends for the same rule and agreement. | `checkInterval` is accepted and stored on temporal triggers, but current hosted evaluation uses a system-wide temporal sweep. Do not rely on `checkInterval` as a per-rule schedule or cadence guarantee. Each due temporal evaluation has one firing identity before recipients and channels are resolved. Re-evaluating the same firing does not create another logical notification. Each resolved recipient and channel receives its own notification effect. Temporal notification payloads do not include a `transition` block. ## Recipient resolution Each notification rule has `recipients`. Shodai resolves those tokens before emitting notification webhook events. | Recipient token | Resolution behavior | | --------------------------------------------- | ---------------------------------------------------------------------------------------- | | Variable key, such as `customerWalletAddress` | Reads that variable value and resolves it to an email. | | `*` | Scans agreement variable values for wallet-address-shaped strings and resolves each one. | | `@observers` | Uses observer email addresses on the agreement. | Resolved raw values are converted to email addresses in this order: 1. participant email matching the wallet address or matching the variable key 2. companion email variable, such as `customerEmail` for `customerWalletAddress` 3. direct email value, when the raw value itself contains an email address Literal email strings are not collected when placed directly in `recipients`. To notify a literal email address, pass it as an observer or as the value of a recipient variable. Resolved email addresses are deduped case-insensitively. Unresolved recipients are skipped and logged. ## Content interpolation `subject`, `title`, and `body` support `${variable}` interpolation. For `onTransition` notifications, interpolation context includes the agreement variables plus: * `agreementName` * `agreementId` * `fromState` * `toState` * `input` For temporal notifications, interpolation context includes the stored agreement variable snapshot plus: * `agreementName` * `agreementId` Unknown variables remain in the rendered string as `${variableName}`. ## Payload shape An `onTransition` notification webhook includes notification content and transition context: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "id": "effect_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "type": "agreement.notification.triggered", "apiVersion": "2026-06-01", "createdAt": "2026-06-02T18:00:00.000Z", "data": { "agreementId": "agr_123", "agreementName": "Advisory Retainer", "templateId": "did:template:service-retainer-v0-1", "notificationTemplateId": "external:principal-1:agr_123", "ruleId": "payment-received", "triggerType": "onTransition", "recipient": "client@example.com", "notification": { "subject": "Payment received for Advisory Retainer", "title": "Work can begin", "body": "Advisory Retainer moved to WORK_IN_PROGRESS.", "ctaLabel": "View agreement" }, "variables": { "agreementName": "Advisory Retainer", "agreementId": "agr_123", "fromState": "AWAITING_PAYMENT", "toState": "WORK_IN_PROGRESS", "input": "submitInitialPaymentProof" }, "transition": { "fromState": "AWAITING_PAYMENT", "toState": "WORK_IN_PROGRESS", "inputId": "submitInitialPaymentProof", "occurredAt": "2026-06-02T17:59:58.000Z" } } } ``` A temporal notification webhook omits `transition`: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "id": "effect_fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210", "type": "agreement.notification.triggered", "apiVersion": "2026-06-01", "createdAt": "2026-06-04T18:00:00.000Z", "data": { "agreementId": "agr_123", "agreementName": "Advisory Retainer", "templateId": "did:template:service-retainer-v0-1", "notificationTemplateId": "external:principal-1:agr_123", "ruleId": "invoice-due-soon", "triggerType": "temporal", "recipient": "client@example.com", "notification": { "subject": "Payment due soon for Advisory Retainer", "body": "Payment is due soon for agreement agr_123." }, "variables": { "agreementName": "Advisory Retainer", "agreementId": "agr_123", "paymentDueAt": "2026-06-06T18:00:00.000Z" } } } ``` One resolved rule-recipient pair produces one `agreement.notification.triggered` event. That event is delivered as signed POST requests to each active webhook subscription that matches the event type and filters. A delivery's opaque `id` remains the same across retries and redeliveries, so receivers can dedupe repeated attempts. Different subscriptions receive distinct delivery IDs. ## Filter notification events Notification webhooks support these filters: | Filter | Matches | | -------------- | ---------------------------------------- | | `agreementIds` | `data.agreementId` | | `templateIds` | `data.templateId` | | `ruleIds` | `data.ruleId` | | `inputIds` | `data.transition.inputId` when present | | `fromStates` | `data.transition.fromState` when present | | `toStates` | `data.transition.toState` when present | Temporal notification payloads do not include `transition`, so they do not match `inputIds`, `fromStates`, or `toStates` filters. ## Handle notification events Your receiver handles notification webhooks like any other Shodai webhook: 1. verify the signature with the subscription secret 2. store and dedupe by event `id` 3. return `2xx` after durable receipt 4. perform your final side effect asynchronously For example, the Shodai Reference App converts vendored notification templates to `external_webhook`, lets hosted Shodai services evaluate transition and temporal rules, receives `agreement.notification.triggered`, and sends final email through its own AWS SES configuration. ## Related pages * [Receive webhooks](/webhooks/receive-webhooks) * [Agreement activity webhooks](/webhooks/agreement-activity-webhooks) * [Shodai Reference App](/examples/reference-app) * [TypeScript client](/sdks/typescript-client) # Receive webhooks Source: https://docs.shodai.network/webhooks/receive-webhooks Register signed webhook endpoints, verify Shodai deliveries, test receivers, and choose which webhook event families your integration receives. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Webhooks are the signed delivery mechanism Shodai uses to send compact events to your backend. A webhook subscription belongs to a Shodai account, points to one HTTPS receiver URL, and can receive one or more subscribed event families. Use this page for subscription setup, verification, retries, testing, and shared delivery behavior. Use [Agreement activity webhooks](/webhooks/agreement-activity-webhooks) for agreement lifecycle reconciliation, and [Notification webhooks](/webhooks/notification-webhooks) for notification-rule deliveries. ## Webhook taxonomy | Concept | Event type | Use it for | | --------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | Webhooks | N/A | Shared subscription, signing, delivery, retry, test, and filtering infrastructure. | | Agreement activity webhooks | `agreement.transitioned` | Mirroring or reconciling agreement lifecycle changes. | | Notification webhooks | `agreement.notification.triggered` | Receiving rule-fired notification content after Shodai evaluates notification templates. | | Test deliveries | `webhook.test` | Testing a receiver with a real signed POST. This is a delivery payload type, not a subscribable `eventTypes` value. | `agreement.transitioned` and `agreement.notification.triggered` are the only subscribable event types. A single subscription can receive either or both. `agreement.transitioned` is delivered for every finalized input, including inputs that do not change the state name: such deliveries carry `fromState` equal to `toState`. Treat the event as "a finalized input advanced this agreement", not strictly "the state name changed" — it is the signal to re-read agreement state and input history. Notification rules are unaffected: they fire only on genuine state entry. Webhook deliveries are informational. Do not trigger value transfers or other irreversible actions from a delivery alone: on-chain value movement belongs in the agreement's own on-chain actions, where the chain enforces it, and anything irreversible on your side should be confirmed against on-chain state rather than a delivered event payload. ## How webhooks are managed You can manage webhook subscriptions in the Developer Portal or through the Agreements API and TypeScript SDK. Both paths manage subscriptions for the same Shodai account. The API-key path uses `/v0/webhooks`, while the Developer Portal uses your signed-in account. If a webhook is created with an API key, `createdByApiKeyId` is audit context, not the ownership boundary. The response field `principalId` identifies the owning Shodai account. Webhook management requires `webhooks.read` and `webhooks.write` access for the account. For key setup and `401` or `403` troubleshooting, see [Authentication](/authentication). ## Before you start You need: * a Shodai API key or Developer Portal access * a public HTTPS endpoint that can receive `POST` requests * server-side access to the exact raw request body before JSON parsing * a secure place to store the webhook signing secret returned at creation ## Register a webhook Use the TypeScript client when you are already using the SDK: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { ApiClient } from '@shodai-network/agreements-api-client'; const client = new ApiClient({ environment: 'testnet', apiKey: process.env.API_KEY, }); const created = await client.createWebhook({ url: 'https://example.com/shodai/webhooks', eventTypes: ['agreement.transitioned', 'agreement.notification.triggered'], filters: { templateIds: ['did:template:service-retainer-v0-1'], }, }); console.log(created.id); console.log(created.secret); ``` The create response includes `secret` once. Store it immediately in your application's secret manager. Shodai does not include the signing secret when you list, get, update, disable, or test webhook subscriptions. If the secret was not stored from the create response, create a new webhook subscription. For raw HTTP integrations, call `POST /v0/webhooks` with the same `url`, optional `eventTypes`, and optional `filters` fields. Use the generated Webhooks pages in the API Reference group for exact request and response schemas. ## Choose event types On create, omitted, `null`, or empty `eventTypes` defaults the subscription to `agreement.transitioned`. On update, omitted `eventTypes` leaves the existing event types unchanged. An explicit empty or `null` `eventTypes` value resets the subscription to `agreement.transitioned`. Subscribe to `agreement.notification.triggered` only when your integration also attaches `external_webhook` notification rules to agreements. A subscription alone does not create notification-triggered events. See [Notification webhooks](/webhooks/notification-webhooks). ## Verify deliveries Prefer the SDK receiver helper instead of hand-rolling signature verification: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { constructWebhookEvent, WebhookVerificationError, } from '@shodai-network/agreements-api-client/webhooks'; export async function receiveWebhook(request: Request) { const rawBody = await request.text(); try { const event = constructWebhookEvent( rawBody, request.headers, process.env.SHODAI_WEBHOOK_SECRET!, ); await storeWebhookEvent(event); return new Response(null, { status: 204 }); } catch (error) { if (error instanceof WebhookVerificationError) { return Response.json({ error: 'invalid_webhook' }, { status: 400 }); } throw error; } } ``` Verify the raw request body before trusting parsed JSON. The helper checks: * `x-shodai-webhook-id` * `x-shodai-webhook-timestamp` * `x-shodai-webhook-signature` * timestamp tolerance, which defaults to `300` seconds * event envelope shape, supported `apiVersion`, supported event type, and header/body event ID match The signature header uses `sha256=`. The signed message is `${timestamp}.${rawBody}`, where `timestamp` is the `x-shodai-webhook-timestamp` header and `rawBody` is the exact request body bytes. Shodai signs the message with HMAC-SHA256 and the subscription secret. ## Respond to deliveries Return a `2xx` response only after signature verification and durable receipt. A common pattern is: 1. verify the signature with `constructWebhookEvent(...)` 2. insert the event into durable storage keyed by event `id` 3. treat duplicate event IDs as already received 4. return `2xx` 5. process business side effects asynchronously Your application owns deduplication, queueing, business side effects, logging, and observability. The event `id` is an opaque deterministic delivery identity. Retries and redeliveries for the same subscription carry the same `id` and payload; store it as an idempotency key instead of parsing its format. Delivery remains at-least-once, so duplicate attempts are expected. ## Understand the event envelope Every webhook delivery uses the same event envelope: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "id": "effect_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "type": "agreement.transitioned", "apiVersion": "2026-06-01", "createdAt": "2026-06-02T18:00:00.000Z", "data": {} } ``` The `data` shape depends on `type`. * `agreement.transitioned` contains compact transition data. See [Agreement activity webhooks](/webhooks/agreement-activity-webhooks). * `agreement.notification.triggered` contains interpolated notification content for one resolved rule-recipient pair. See [Notification webhooks](/webhooks/notification-webhooks). * `webhook.test` contains an empty `data` object. ## Filter deliveries Filters apply before delivery. Filter values are exact string matches. Multiple values inside one filter field act like OR. Different filter fields combine like AND. Empty or omitted filters mean all subscribed events for the account. A supplied filter key must contain at least one non-blank value; an empty array or an array containing a blank or whitespace-only member is rejected with 400. | Filter | `agreement.transitioned` | `agreement.notification.triggered` | | -------------- | --------------------------------------- | -------------------------------------------------------------------------------------- | | `agreementIds` | Matches `data.agreementId`. | Matches `data.agreementId`. | | `templateIds` | Matches `data.templateId`. | Matches `data.templateId`. | | `inputIds` | Matches `data.inputId`. | Matches `data.transition.inputId` when the notification payload has transition data. | | `fromStates` | Matches `data.fromState`. | Matches `data.transition.fromState` when the notification payload has transition data. | | `toStates` | Matches `data.toState`. | Matches `data.transition.toState` when the notification payload has transition data. | | `ruleIds` | Not used for agreement activity events. | Matches `data.ruleId`. | Temporal notification webhooks do not include a `transition` block. They do not match `inputIds`, `fromStates`, or `toStates` filters. ## Test a webhook Use `client.testWebhook(...)` or `POST /v0/webhooks/{id}/test` to send a real signed `webhook.test` delivery to the subscription URL: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const result = await client.testWebhook(created.id); console.log(result.ok); console.log(result.deliveryId); console.log(result.status); ``` The test response reports the immediate delivery attempt. `ok` is `true` only when that attempt succeeds. Failed tests can include `status`, `responseStatus`, and `error`. Disabled subscriptions cannot be tested. ## Disable a webhook Use `client.deleteWebhook(webhookId)` or `DELETE /v0/webhooks/{id}` to disable a subscription: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const disabled = await client.deleteWebhook(created.id); console.log(disabled.status); ``` The route disables the subscription and returns the subscription with `status: "disabled"`. It does not hard-delete the subscription record. ## Handle failures and retries Delivery status depends on your receiver response: | Receiver result | Shodai behavior | | ------------------------------ | -------------------------------- | | `2xx` | Marks the delivery as succeeded. | | Network failure or no response | Retries when attempts remain. | | `5xx` | Retries when attempts remain. | | `4xx` | Marks the delivery as failed. | By default, Shodai makes up to `5` total delivery attempts. Retry scheduling is checked about every `60` seconds. Backoff starts at `60` seconds and doubles, capped at `60` minutes. Redirects are not followed. If a subscription is disabled or missing before a retry, the pending delivery is marked failed. ## Use public HTTPS URLs Webhook URLs must be valid HTTP or HTTPS URLs and must not include credentials. In normal hosted usage, Shodai requires HTTPS and rejects localhost or private-network targets, including hosts that resolve to private addresses. For local development, expose your local receiver through a public HTTPS tunnel and register the tunnel URL. ## Troubleshooting | Problem | What to check | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | Missing secret | Use the secret stored from the create response. If it was not stored, create a new webhook subscription. | | Signature mismatch | Verify against the exact raw body and the correct subscription secret before JSON parsing. | | Timestamp rejected | Check receiver clock drift or pass an explicit tolerance to `constructWebhookEvent(...)` if your receiver needs one. | | No activity events received | Check subscription status, filters, event types, account ownership, and whether the agreement belongs to the same account. | | No notification events received | Check subscription status, event types, deploy-time `notificationTemplate`, `external_webhook` rule channels, rule triggers, and recipient resolution. | | Test fails | Inspect `responseStatus` and `error`. `4xx` is terminal; network failures and `5xx` can retry when attempts remain. | | Local URL rejected | Use a public HTTPS tunnel for local development. | ## Related pages * [Agreement activity webhooks](/webhooks/agreement-activity-webhooks) * [Notification webhooks](/webhooks/notification-webhooks) * [TypeScript client](/sdks/typescript-client) * [Authentication](/authentication) * Use the Webhooks pages in the API Reference group for generated endpoint schemas. # Author Agreement JSON Source: https://docs.shodai.network/workflow/author-agreement-json Learn how to make good authoring decisions when turning a real business workflow into agreement JSON. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Author agreement JSON by modeling the real business workflow first, then expressing that workflow through variables, content, states, inputs, issuers, transitions, and initialization. ## What is an agreement? An agreement is an authored JSON artifact that combines readable agreement text, the business facts that fill that text, participant roles, and lifecycle rules for what can happen after deployment. The [agreement data standard](/system-architecture/data-standard) explains the agreement definition model in architectural context. This page focuses on the authoring decisions that turn a real business workflow into that JSON structure. ## Start with the workflow Before writing JSON, answer these questions in plain language: 1. Who participates? 2. What facts does the agreement depend on? 3. What should the agreement say? 4. What state does it begin in? 5. What can happen later? 6. Who is allowed to do each later thing? 7. What changes when those things happen? When those answers are vague, the JSON usually becomes harder to validate, deploy, and operate. ## Programmatic authoring Agreement definitions are data. They can be authored manually, generated by applications, or produced by agents, as long as they conform to the agreement data standard and target the capabilities of the current onchain execution engine. Programmatic authoring is useful when agreements are created dynamically at runtime, such as agent-to-agent coordination, marketplace interactions, resource-sharing arrangements, or short-lived service agreements. Generated agreements should still be validated, reviewable, and understandable before deployment because humans, applications, and agents may rely on their behavior. ## Model only facts that do work A variable should earn its place in the agreement. | A variable is useful when it... | Why it matters | | --------------------------------------------------- | ---------------------------------------------------- | | appears in the agreement text | Readers and signers need the value. | | must exist when the agreement starts | Deployment must supply or map the value. | | is submitted later as part of an event | Operation needs the value for an input. | | helps determine who may act | Issuer rules need the value. | | helps explain the agreement in interfaces or agents | Operators need the value to understand the workflow. | ## Make variables understandable Each variable should explain what it means and how it should be used. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "retainerFloor": { "type": "uint256", "name": "Retainer Floor", "helperText": "Threshold at which retainer topup requested", "description": "The minimum amount expected to be held in the retainer from month to month. If an invoice would reduce the retainer below this amount, topup of the retainer should be requested.", "validation": { "required": true, "min": 0 } } } ``` The field is useful because it names the number, explains its workflow meaning, and constrains the expected range. ## Treat participant addresses as roles Participant addresses define who the agreement is about and who may act inside it. If a variable represents a participant, mark it as a participant role. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "serviceProviderRepresentative": { "type": "address", "subtype": "participant", "name": "Service Provider Representative", "helperText": "Wallet address designating the service provider representative", "description": "Participant address used when the service provider representative submits and reviews agreement inputs." }, "clientRepresentative": { "type": "address", "subtype": "participant", "name": "Client Representative", "helperText": "Wallet address designating the client representative", "description": "Participant address used when the client representative submits and reviews agreement inputs." } } ``` Those roles later connect to [deployment participant mappings](/workflow/deploy-an-agreement#assemble-deployment-context) and [input `issuer` rules](/workflow/operate-a-deployed-agreement#choose-a-valid-authored-input). ## Use validation to sharpen clear fields Validation should make an already-clear field precise. The examples use validation for required values, numeric minimums, and string length. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "awaitingPaymentPaymentLink": { "type": "string", "subtype": "url", "name": "Link to Payment Proof", "helperText": "Enter transaction url", "description": "Block explorer link for external payment or settlement proof", "validation": { "required": true, "minLength": 1 } } } ``` Use this pattern: decide the fact, describe the fact clearly, then add validation that reflects the actual expectation. ## Keep content and execution aligned Agreement content should read like a real document while using the same variables the rest of the agreement depends on. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "content": { "type": "md", "data": "# ${variables.retainerTitle}\n\n${variables.retainerDescription}\n\n## Participants\n\n- **Service Provider:** ${variables.serviceProviderName}\n- **Client:** ${variables.clientName}" } } ``` The content should not imply facts or behavior that the variables and lifecycle do not support. ## Author states, inputs, issuers, and transitions as workflow States should be recognizable moments in the business process. Inputs should be real events someone can choose. Issuers should encode who is responsible for each event. Transitions should show how valid events move the lifecycle. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "transitions": [ { "from": "WORK_IN_PROGRESS", "to": "INVOICE_SUBMITTED", "conditions": [ { "type": "isValid", "input": "submitInvoice" } ] } ] } ``` A lifecycle is strong when someone can read the states, inputs, and transitions together and understand the agreement process. The state/transition model gives the agreement a behavioral map. It helps authors, counterparties, applications, and agents understand how the agreement can progress before it is deployed. This visual representation is not merely illustrative. It reflects the authored behavior: the states the agreement can occupy, the inputs accepted in each state, who may submit those inputs, and the transitions that can follow. ## Use initialization for the starting condition `execution.initialize` defines what must exist when the agreement begins. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "initialize": { "initialState": "AWAITING_PAYMENT", "data": { "serviceProviderRepresentative": "${variables.serviceProviderRepresentative}", "clientRepresentative": "${variables.clientRepresentative}", "retainerTitle": "${variables.retainerTitle}" } } } ``` Facts required at deployment belong in initialization. Facts produced later by real-world events belong in inputs. ## Authoring checklist Before validation, confirm that: 1. the agreement models a real workflow 2. each variable has a clear purpose 3. participant roles are explicit 4. agreement text uses the same vocabulary as the model 5. states and inputs describe real lifecycle moments 6. issuers match responsibility 7. transitions reflect the intended process 8. initialization includes the values required at the start ## Next step Use [Validate Agreement Structure](/workflow/validate-agreement-structure) to check participant roles, input IDs, state IDs, and warnings before deployment preflight. # Deploy an Agreement Source: https://docs.shodai.network/workflow/deploy-an-agreement Turn structurally valid agreement JSON into a live agreement with deployment values, participant mappings, preflight checks, and EIP-712 authorization. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Deployment combines authored agreement JSON with a target `chainId`, live `initValues`, participant wallet mappings, optional observers, and a [signed EIP-712 permit](/reference/eip-712-signing) so the API can create a live agreement. [Structural validation](/workflow/validate-agreement-structure) checks the authored agreement. Deployment preflight checks the assembled deployment request. Deployment with permit creates the agreement. Do not sign deployment permits against raw caller input when participant mappings change values that are hashed or encoded into the signed message. Sign the effective post-mapping values returned by deployment preflight. Each hosted API environment can support multiple agreement deployment chains at the same time. The `testnet` environment supports Linea Sepolia, Ethereum Sepolia, and Base Sepolia; the `production` environment supports Linea Mainnet and Base Mainnet. Choose a supported `chainId` for every deployment preflight and deploy request, and use a `publicClient` connected to that same chain before signing. ## SDK deployment path For TypeScript integrations, use the SDK for both deployment preflight and permit signing. Before deployment, make sure your integration has a `walletClient` for the deploying signer and a `publicClient` connected to the target chain/RPC. The signer must be able to produce an EIP-712 signature; the SDK cannot deploy from a wallet address alone. For automated tests that only need signatures, see [Create a test-only wallet client](/sdks/typescript-client#create-a-test-only-wallet-client). ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const chainId = 59141; const validation = await client.validateDeployment({ agreement, chainId, initValues, participants, observers, }); console.log(validation.variables); ``` ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -sS "$BASE_URL/v0/agreements/validate" \ -X POST \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ --data @deployment-preflight.json ``` Review `validation.variables`, `validation.participants`, `validation.observers`, `validation.contributors`, and `validation.warnings` before signing. In raw HTTP flows, review the equivalent fields in the preflight response. Keep the selected `chainId` with the deployment request so the preflight, permit signature, and deploy call target the same chain. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { deployAgreementWithPermit } from '@shodai-network/agreements-api-client'; const deployed = await deployAgreementWithPermit({ client, walletClient, publicClient, chainId, agreement, displayName: 'Advisory Retainer', initValues: validation.variables, participants, observers, }); ``` `validation.variables` contains the effective deployment values the SDK signs after participant mappings are applied. Keep the same `participants` array in the helper call so hosted agreement context still records the participant mappings. `publicClient` must be connected to the selected chain/RPC. The helper resolves the chain-specific factory context, reads the current permit nonce, signs the permit, and submits `deployWithPermit`. Participant-derived values in `validation.variables` are the effective values the SDK signs for deployment. The `participants` array still belongs in `deployAgreementWithPermit(...)` for hosted participant context. Use the raw request flow below when you need to inspect the exact payload shape or when you are not using the TypeScript client. ## Raw deployment flow Provide the authored `agreement`, target `chainId`, `initValues` for values required at initialization, `participants` for participant-role wallet mappings, and optional `observers`. Preflight uses these fields; deployment adds `displayName` and signed permit fields. `chainId` must be one of the agreement deployment chains supported by the target API environment. Include it in both deployment preflight and deploy-with-permit requests so validation, EIP-712 signing, and transaction submission all target the same chain. Send the full authored agreement JSON in `agreement`. The nested agreement below is shortened to its top-level sections for readability, but the request shape includes the required `agreement` field. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "agreement": { "metadata": { "templateId": "did:template:service-retainer-manual-balance-v0-1", "name": "Service Retainer" }, "variables": { "serviceProviderRepresentative": { "type": "address", "subtype": "participant", "validation": { "required": true } }, "clientRepresentative": { "type": "address", "subtype": "participant", "validation": { "required": true } } }, "content": { "type": "md", "data": "..." }, "execution": { "initialize": { "initialState": "AWAITING_PAYMENT", "data": {} }, "states": {}, "inputs": {}, "transitions": [] } }, "chainId": 59141, "initValues": { "retainerTitle": "Advisory Retainer", "retainerDescription": "Monthly strategic support with invoice-driven replenishment.", "serviceProviderName": "Provider LLC", "clientName": "Client Inc", "retainerCeiling": 1000, "retainerFloor": 200, "paymentInstructions": "Wire funds using the invoice instructions." }, "participants": [ { "variableKey": "serviceProviderRepresentative", "walletAddress": "0x1111111111111111111111111111111111111111" }, { "variableKey": "clientRepresentative", "walletAddress": "0x2222222222222222222222222222222222222222", "email": "client@example.com" } ], "observers": [ "legal@example.com", "ops@example.com" ] } ``` Participant wallet identities belong in `participants`, not duplicated in `initValues`, when the agreement models them as participant variables. A creator can provide a participant wallet address whether or not that wallet is already linked to a Shodai account. If the wallet is linked later, the owning account can discover the agreement through that address; linking does not rewrite the participant's recorded provenance. See [Link Wallets and Access Agreements](/workflow/link-a-wallet-and-access-agreements). Call `POST /v0/agreements/validate` before signing. ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -sS "$BASE_URL/v0/agreements/validate" \ -X POST \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ --data @deployment-preflight.json ``` Deployment preflight checks authored agreement JSON, `initValues`, participant mappings, observers, and normalized variables. It does not deploy the agreement and does not validate permit signatures. The raw HTTP preflight response is wrapped in a `data` envelope. `client.validateDeployment(...)` unwraps `data` and returns the preflight summary directly. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "data": { "templateId": "did:template:service-retainer-manual-balance-v0-1", "participantVariableKeys": [ "serviceProviderRepresentative", "clientRepresentative" ], "participants": [ { "variableKey": "serviceProviderRepresentative", "walletAddress": "0x1111111111111111111111111111111111111111" }, { "variableKey": "clientRepresentative", "walletAddress": "0x2222222222222222222222222222222222222222", "email": "client@example.com" } ], "observers": [ "legal@example.com", "ops@example.com" ], "variables": { "serviceProviderRepresentative": "0x1111111111111111111111111111111111111111", "clientRepresentative": "0x2222222222222222222222222222222222222222", "retainerTitle": "Advisory Retainer" }, "contributors": [ "0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222" ], "warnings": [] }, "meta": { "apiVersion": "v0", "requestId": "req_123" } } ``` Resolve warnings before signing. If participant mappings changed any value included in the permit, sign the post-mapping values from `data.variables`. The wallet in `signer` authorizes deployment by signing EIP-712 typed data. The API uses that authorization to submit the transaction. For direct API deploy, the hosted record `owner` comes from the authenticated Shodai account's primary wallet and may differ from `signer`. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "signer": "0x1111111111111111111111111111111111111111", "deadline": 1776219513, "signature": { "v": 27, "r": "0x...", "s": "0x..." } } ``` The TypeScript client uses a one-hour default permit lifetime through `computeDefaultDeadlineSeconds()`. Use a shorter deadline if your integration requires a tighter replay window, and regenerate the signature whenever the deadline expires. If you construct the deploy permit typed data directly, read the current nonce from `AgreementFactory.nonces(signer)` on the target chain through a live RPC before signing. Do not hardcode `0`; a successful deploy permit consumes the nonce, so the same signature cannot be reused after deployment or any other signer nonce change. Submit the signed request to `POST /v0/agreements/deploy-with-permit`. Build `deploy-with-permit.json` from the same full `agreement` object and deployment context you preflighted, plus `displayName` and the permit fields. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "agreement": { "metadata": { "templateId": "did:template:service-retainer-manual-balance-v0-1", "name": "Service Retainer" }, "variables": { "serviceProviderRepresentative": { "type": "address", "subtype": "participant", "validation": { "required": true } }, "clientRepresentative": { "type": "address", "subtype": "participant", "validation": { "required": true } } }, "content": { "type": "md", "data": "..." }, "execution": { "initialize": { "initialState": "AWAITING_PAYMENT", "data": {} }, "states": {}, "inputs": {}, "transitions": [] } }, "displayName": "Advisory Retainer", "chainId": 59141, "initValues": { "retainerTitle": "Advisory Retainer", "retainerDescription": "Monthly strategic support with invoice-driven replenishment.", "serviceProviderName": "Provider LLC", "clientName": "Client Inc", "retainerCeiling": 1000, "retainerFloor": 200, "paymentInstructions": "Wire funds using the invoice instructions." }, "participants": [ { "variableKey": "serviceProviderRepresentative", "walletAddress": "0x1111111111111111111111111111111111111111" }, { "variableKey": "clientRepresentative", "walletAddress": "0x2222222222222222222222222222222222222222", "email": "client@example.com" } ], "observers": [ "legal@example.com", "ops@example.com" ], "signer": "0x1111111111111111111111111111111111111111", "deadline": 1776219513, "signature": { "v": 27, "r": "0x...", "s": "0x..." } } ``` ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -sS "$BASE_URL/v0/agreements/deploy-with-permit" \ -X POST \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ --data @deploy-with-permit.json ``` A successful raw HTTP response returns an envelope with the deployed agreement record in `data`. SDK deployment helpers unwrap `data` and return the agreement record directly. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "data": { "id": "agr_123", "status": "Deployed", "address": "0x3333333333333333333333333333333333333333", "chainId": 59141, "owner": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "participants": [ { "variableKey": "serviceProviderRepresentative", "walletAddress": "0x1111111111111111111111111111111111111111" }, { "variableKey": "clientRepresentative", "walletAddress": "0x2222222222222222222222222222222222222222" } ], "observers": [ "legal@example.com", "ops@example.com" ], "state": "AWAITING_PAYMENT" }, "meta": { "apiVersion": "v0", "requestId": "req_123" } } ``` The top-level `owner` is the hosted record owner associated with the authenticated Shodai account. Carry forward the agreement ID, deployed address, chain ID, hosted record owner, participant context, observer context, and current state. Use the returned `chainId` for later agreement reads, input signing, and chain-filtered agreement lists. The API returns after the deployment receipt is mined, at roughly one confirmation. Chain attribution and state in this response are provisional; the API does not wait synchronously for finality. Canonical projection and deploy-triggered webhooks follow later, after the deployment block reaches the shared finality pin at `head - 20`. After deployment, the agreement's core definition is fixed. This is intentional: the deployed agreement becomes a shared operational source of truth that parties can inspect and rely on. If the agreement needs to change, model the change explicitly through the agreement itself, deploy a new agreement, or use an amendment pattern appropriate to the application. ## Helper boundaries The higher-level `deployAgreementWithPermit(...)` helper signs and submits the deployment request, but it does not replace the deployment preflight review step. Use `client.validateDeployment(...)` first when participant mappings or deployment values affect the permit payload. Low-level `signDeployWithPermit(...)` requires an explicit `deadline`; high-level `deployAgreementWithPermit(...)` defaults to `computeDefaultDeadlineSeconds()`. ## Keep deployment boundaries clear | Boundary | What to remember | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `POST /v0/agreements/validate-template` | Checks authored agreement JSON only. | | `POST /v0/agreements/validate` | Checks agreement JSON plus deployment context and normalized variables. | | `POST /v0/agreements/deploy-with-permit` | Creates the live agreement with signed authorization. | | `chainId` | Selects the target deployment chain; include the same value in preflight, signing, and deploy-with-permit requests. | | `participants` | Maps participant-role variables to wallet addresses. | | `observers` | Adds optional email context; it is not participant identity. | | deployment permit | Authorizes deployment; it does not submit post-deploy inputs. | ## Related pages * [Validate Agreement Structure](/workflow/validate-agreement-structure) * [Operate a Deployed Agreement](/workflow/operate-a-deployed-agreement) * [EIP-712 Signing Reference](/reference/eip-712-signing) * [Errors and troubleshooting](/reference/errors-and-troubleshooting) # Link Wallets and Access Agreements Source: https://docs.shodai.network/workflow/link-a-wallet-and-access-agreements Prove wallet control, link wallets to your Shodai account, and understand how linked wallets affect agreement visibility. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Link each wallet you control to your Shodai account once. External API requests authenticated as that account can then discover agreements that name any linked address, whether the agreement already exists or is created later. Assigning an address to an agreement does not require the wallet to be linked first. It also does not create an invitation or acceptance step. ## Keep identity, visibility, and authority separate | Concept | What it means | What it does not mean | | ---------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | Linked-wallet identity | A signature proves that your Shodai account controls a wallet address. | Shodai does not take custody of the wallet or gain signing access. | | Agreement visibility | Agreements can be listed when a participant address matches any wallet linked to the authenticated account. | Visibility alone does not authorize agreement inputs. | | Input authority | The authored input `issuer` identifies eligible signing wallets, and the submission must carry a valid signature. | Linking a wallet does not grant blanket permission to operate an agreement. | ## Link a wallet in the Developer Portal Sign in to the Shodai account that should gain wallet-based agreement visibility. Under **Visibility**, select **Connected wallets**. Choose **Connect wallet**, select the browser wallet, and sign the challenge. The signature proves control without transferring custody. Confirm that the wallet address appears on your account. Credentials for that Shodai account can now use the linked address as an agreement-visibility signal. ## Link a wallet through the External API Use an authenticated Shodai account credential with `agreements.write` and a wallet client for the address you are linking. The example uses an OAuth bearer access token. `X-API-Key` is the alternative credential header. Send the wallet address to `POST /v0/siwe/nonce`. The response supplies the nonce and issued-at value for the EIP-4361 message. Keep the domain, URI, address, nonce, chain ID, and issued-at value consistent. Sign the exact message that you submit for verification. Submit the address, exact message, signature, domain, and chain ID to `POST /v0/siwe/verify`. A successful response links the verified address to the authenticated Shodai account. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { createWalletClient, http, type Hex } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { lineaSepolia } from 'viem/chains'; import { createSiweMessage } from 'viem/siwe'; const baseUrl = process.env.SHODAI_API_BASE_URL!.replace(/\/+$/, ''); const apiUrl = new URL(baseUrl); const accessToken = process.env.SHODAI_ACCESS_TOKEN!; const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as Hex); const walletClient = createWalletClient({ account, chain: lineaSepolia, transport: http(process.env.RPC_URL), }); const chainId = lineaSepolia.id; const address = account.address; const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }; const nonceResponse = await fetch(`${baseUrl}/v0/siwe/nonce`, { method: 'POST', headers, body: JSON.stringify({ address }), }); if (!nonceResponse.ok) { throw new Error( `Nonce request failed: ${nonceResponse.status} ${await nonceResponse.text()}`, ); } const { data: challenge } = await nonceResponse.json(); const message = createSiweMessage({ domain: apiUrl.host, address, statement: 'Prove control of this wallet to Shodai.', uri: apiUrl.origin, version: '1', chainId, nonce: challenge.nonce, issuedAt: new Date(challenge.issuedAt), }); const signature = await walletClient.signMessage({ account, message }); const verificationResponse = await fetch(`${baseUrl}/v0/siwe/verify`, { method: 'POST', headers, body: JSON.stringify({ address, message, signature, domain: apiUrl.host, chainId, }), }); if (!verificationResponse.ok) { throw new Error( `Wallet verification failed: ${verificationResponse.status} ${await verificationResponse.text()}`, ); } const { data: verifiedWallet } = await verificationResponse.json(); console.log(verifiedWallet); // Use a credential with agreements.read for the same Shodai account. const agreementsResponse = await fetch(`${baseUrl}/v0/agreements`, { headers: { Authorization: `Bearer ${process.env.SHODAI_READ_ACCESS_TOKEN!}`, }, }); if (!agreementsResponse.ok) { throw new Error( `Agreement list failed: ${agreementsResponse.status} ${await agreementsResponse.text()}`, ); } const { data: agreements } = await agreementsResponse.json(); console.log(agreements); ``` See [Create wallet challenge](/reference/api/wallet-access/create-wallet-verification-challenge) and [Verify and link wallet](/reference/api/wallet-access/verify-and-link-wallet) for request and response details. ## What becomes visible List agreements with an `agreements.read` credential for the same Shodai account. Agreements that already name the linked address can appear after linking, and future agreements that name it require no separate acceptance step. Linking and unlinking affect only visibility derived from the account-wallet association. Creator, owner, contributor, or observer access can keep an agreement visible through another documented access signal. ## Interpret `walletBinding` as provenance The SIWE verification response uses `verified_via_siwe` to report how the account wallet was verified. An agreement participant's `walletBinding` separately reports how that address was established on the agreement record. | Participant value | Provenance | | ------------------- | -------------------------------------------------------------------- | | `partner_asserted` | An API caller supplied the participant address directly. | | `verified_via_auth` | Shodai resolved the address through participant identity. | | `verified_via_siwe` | SIWE proof established the participant binding when it was recorded. | These values are not current access decisions or input-permission flags. Linking an address later does not rewrite an existing `partner_asserted` participant record. ## Understand the authority boundary Linking proves wallet control. It does not transfer custody, and Shodai cannot sign agreement inputs for you. A wallet may submit an input only when the authored input [`issuer`](/workflow/author-agreement-json#author-states-inputs-issuers-and-transitions-as-workflow) allows it and the submission includes a valid signature from that wallet. ## Unlink a self-linked wallet You can remove a self-linked SIWE wallet from **Visibility > Connected wallets** in the Developer Portal. Unlinking removes visibility derived solely from that account-wallet association; relinking restores the identity signal. Dynamic or operator-managed wallets follow different removal rules and are not necessarily self-unlinkable. Continue with [Deploy an Agreement](/workflow/deploy-an-agreement) to assign participant addresses or [Operate a Deployed Agreement](/workflow/operate-a-deployed-agreement) to submit an authorized input. # Operate a Deployed Agreement Source: https://docs.shodai.network/workflow/operate-a-deployed-agreement Read a deployed agreement, submit signed inputs, and confirm state and input-history changes. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Operate a deployed agreement by reading its current state, choosing a valid authored input, signing that input, submitting it, and then rereading state and input history. For TypeScript integrations, use the SDK operation loop first. Endpoint paths later in this page use full API paths under `/v0` for raw HTTP and reference context. Input submission requires a `walletClient` that can sign with the submitting account and a `publicClient` connected to the agreement chain. The signing wallet must be allowed by the [authored input `issuer`](/workflow/author-agreement-json#author-states-inputs-issuers-and-transitions-as-workflow); otherwise the signed input may be well-formed but invalid for the agreement lifecycle. For automated tests that only need signatures, see [Create a test-only wallet client](/sdks/typescript-client#create-a-test-only-wallet-client). Agreement visibility through a linked wallet is not permission to submit every input. See [Link Wallets and Access Agreements](/workflow/link-a-wallet-and-access-agreements) for account visibility; the authored input [`issuer`](/workflow/author-agreement-json#author-states-inputs-issuers-and-transitions-as-workflow) remains the authority rule. This page shows the API-assisted operation path. The same authorization model is grounded in EIP-712 signed inputs and the onchain execution engine. Applications that need direct onchain operation should refer to the [EIP-712 Signing Reference](/reference/eip-712-signing) and [onchain architecture notes](/system-architecture/on-chain) for typed data, contract addresses, and deployment details. ## SDK operation loop ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const agreementRecord = await client.getAgreement(agreementId); const current = await client.getAgreementState(agreementId); ``` Use the hosted agreement record for address, `chainId`, participant, observer, and stored agreement JSON context. Use current state to decide which authored inputs are candidates next. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { submitAgreementInputWithPermit } from '@shodai-network/agreements-api-client'; import type { AgreementJson } from '@shodai-network/agreements-protocol-evm'; const inputRecord = await submitAgreementInputWithPermit({ client, agreementId, walletClient, publicClient, chainId: agreementRecord.chainId, agreementContractAddress: agreementRecord.address!, agreement: agreementRecord.json as AgreementJson, inputId: 'submitInvoice', values, }); ``` Confirm that the input exists, the current state accepts it, the values match the input schema, the signing wallet is allowed by the input `issuer`, and `publicClient` is connected to `agreementRecord.chainId` before submitting. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const next = await client.getAgreementState(agreementId); const inputsPage = await client.listAgreementInputs(agreementId, { limit: 25 }); ``` Use state for the current lifecycle position and input history for audit and chronology. `publicClient` must be connected to the target chain/RPC. Pass `agreementRecord.chainId` so the helper can reject a mismatched client chain before requesting a signature. Low-level `signAgreementInputPermit(...)` requires an explicit deadline; high-level `submitAgreementInputWithPermit(...)` defaults to `computeDefaultDeadlineSeconds()`. ## Raw operation loop Use `GET /v0/agreements/{id}` when your product needs the hosted agreement record and context around it. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "data": { "id": "agr_123", "status": "Deployed", "address": "0x3333333333333333333333333333333333333333", "displayName": "Advisory Retainer", "chainId": 59141, "participants": [ { "variableKey": "serviceProviderRepresentative", "walletAddress": "0x1111111111111111111111111111111111111111" }, { "variableKey": "clientRepresentative", "walletAddress": "0x2222222222222222222222222222222222222222", "email": "client@example.com" } ], "observers": [ "legal@example.com", "ops@example.com" ] }, "meta": { "apiVersion": "v0", "requestId": "req_123" } } ``` Use `GET /v0/agreements/{id}/state` to decide which authored inputs are candidates next. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "data": { "status": "Deployed", "state": "WORK_IN_PROGRESS" }, "meta": { "apiVersion": "v0", "requestId": "req_123" } } ``` Interpret the state against the authored agreement JSON. Confirm that the input exists, the current state accepts it, the values match the input schema, and the signer is allowed by the input `issuer`. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "inputs": { "submitInvoice": { "type": "VerifiedCredentialEIP712", "schema": "verified-credential-eip712.schema.json", "displayName": "Submit Invoice", "data": { "retainerBalanceBeforeInvoice": { "type": "uint256", "name": "Retainer Balance Before Invoice" }, "invoiceLineItems": { "type": "string", "subtype": "invoice-csv", "name": "Invoice Line Items" }, "submitInvoiceComment": "${variables.submitInvoiceComment}" }, "issuer": "${variables.serviceProviderRepresentative.value}" } }, "transitions": [ { "from": "WORK_IN_PROGRESS", "to": "INVOICE_SUBMITTED", "conditions": [ { "type": "isValid", "input": "submitInvoice" } ] } ] } ``` Submit the signed input to `POST /v0/agreements/{id}/input`. The request body does not carry a separate `chainId`; the API uses the deployed agreement record. When constructing the EIP-712 signature, use the record's `chainId` in the typed-data domain and connect your RPC client to that same chain. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "inputId": "submitInvoice", "values": { "retainerBalanceBeforeInvoice": 1000, "invoiceLineItems": "2026-04-01,Advisory services,10,100,1000", "submitInvoiceComment": "Invoice submitted for April services." }, "signer": "0x1111111111111111111111111111111111111111", "deadline": 1776219513, "signature": { "v": 27, "r": "0x...", "s": "0x..." } } ``` The TypeScript client uses a one-hour default permit lifetime through `computeDefaultDeadlineSeconds()`. Use a shorter deadline if your integration requires a tighter replay window, and regenerate the signature whenever the deadline expires. After a successful submission, call `GET /v0/agreements/{id}/state` and `GET /v0/agreements/{id}/inputs` to confirm where the agreement landed and what was recorded. ## Understand the input record When `POST /v0/agreements/{id}/input` succeeds, the API returns an envelope with the input record in `data`. SDK helpers unwrap `data` and return the input record directly. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "data": { "agreementId": "agr_123", "agreementAddress": "0x3333333333333333333333333333333333333333", "chainId": 59141, "inputId": "submitInvoice", "values": { "retainerBalanceBeforeInvoice": 1000, "invoiceLineItems": "2026-04-01,Advisory services,10,100,1000", "submitInvoiceComment": "Invoice submitted for April services." }, "txHash": "0x4444444444444444444444444444444444444444444444444444444444444444", "payload": "0x...", "status": "PENDING", "blockNumber": 123456, "createdAt": "2026-04-27T16:10:00.000Z", "updatedAt": "2026-04-27T16:11:00.000Z" }, "meta": { "apiVersion": "v0", "requestId": "req_123" } } ``` The input record proves the event was accepted and recorded. It does not replace reading current state when your product needs to know where the lifecycle is now. ## Read input history Use `GET /v0/agreements/{id}/inputs` to inspect recorded submissions. The response is paged; use `limit` and `cursor` to move through history. Add `userId`, `inputId`, or `status` filters when you need a narrower audit view. You can also filter by `createdAt` or `updatedAt` with `gt`, `gte`, `lt`, and `lte` operators, and sort by `createdAt` or `updatedAt`. Only one sort field is supported. ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl --globoff -sS "$BASE_URL/v0/agreements/agr_123/inputs?status=FINALIZED&createdAt[gte]=2026-05-01T00:00:00.000Z&sort[createdAt]=desc&limit=25" \ -H "X-API-Key: $API_KEY" ``` ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "data": [ { "agreementId": "agr_123", "agreementAddress": "0x3333333333333333333333333333333333333333", "chainId": 59141, "inputId": "submitInitialPaymentProof", "values": { "awaitingPaymentPaymentLink": "https://example.com/tx/0xaaa", "awaitingPaymentComment": "Initial payment completed." }, "txHash": "0x1111111111111111111111111111111111111111111111111111111111111111", "payload": "0x...", "status": "FINALIZED", "blockNumber": 123450, "createdAt": "2026-04-27T16:05:00.000Z", "updatedAt": "2026-04-27T16:06:00.000Z" }, { "agreementId": "agr_123", "agreementAddress": "0x3333333333333333333333333333333333333333", "chainId": 59141, "inputId": "submitInvoice", "values": { "retainerBalanceBeforeInvoice": 1000, "invoiceLineItems": "2026-04-01,Advisory services,10,100,1000", "submitInvoiceComment": "Invoice submitted for April services." }, "txHash": "0x4444444444444444444444444444444444444444444444444444444444444444", "payload": "0x...", "status": "FINALIZED", "blockNumber": 123456, "createdAt": "2026-04-27T16:10:00.000Z", "updatedAt": "2026-04-27T16:11:00.000Z" } ], "pageInfo": { "limit": 25, "nextCursor": null, "totalCount": 2 }, "meta": { "apiVersion": "v0", "requestId": "req_123" } } ``` ## Handle state synchronization An input appears as `PENDING` as soon as it is submitted, and carries a `txHash` and `blockNumber` once it is in a block. It stays `PENDING` until the transaction is finalized on chain, at which point it becomes `FINALIZED` and the state view reflects the new lifecycle position in the same update. Treat `PENDING` as "submitted, not yet certain" and `FINALIZED` as the point at which the transition is durable. Use input history for audit and chronology. ## Helper boundaries `submitAgreementInputWithPermit(...)` signs the input payload and calls `client.submitAgreementInput(...)` with the resulting signer, deadline, and signature. Use `signAgreementInputPermit(...)` plus `client.submitAgreementInput(...)` when your application needs to sign first and submit later. ## Why a submission may not move the agreement When a submission does not advance the agreement, check: 1. the agreement is in the state that accepts the input 2. the submitted `inputId` exists in the authored agreement 3. the submitted `values` match the input schema 4. the signer is allowed by the input `issuer` 5. the transition condition references that input from the current state 6. the signature has not expired and was [generated for this exact payload](/reference/eip-712-signing) 7. the signature was generated for the deployed agreement's `chainId` and contract address ## Related pages * [Deploy an Agreement](/workflow/deploy-an-agreement) * [EIP-712 Signing Reference](/reference/eip-712-signing) * [Errors and troubleshooting](/reference/errors-and-troubleshooting) # Validate Agreement Structure Source: https://docs.shodai.network/workflow/validate-agreement-structure Check authored agreement JSON, read validation feedback, and distinguish template validation from deployment preflight. For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt). Validate authored agreement JSON with `client.validateTemplate(...)` before preparing deployment values, participant wallet mappings, signer data, or permits. Structural validation checks authored agreement JSON only. [Deployment preflight](/workflow/deploy-an-agreement#preflight-the-deployment-request) checks the authored agreement plus deployment values, participant mappings, observers, and normalized variables. ## When to validate Run structural validation after authoring and before deployment preflight when: 1. participant roles are modeled 2. states and inputs reflect the intended workflow 3. `execution.initialize` is in place 4. you want feedback before assembling deployment context At this point, the question is not "can I deploy?" The question is "does this agreement expose the participant roles, state IDs, and input IDs I expect?" ## Validate the authored agreement ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { ApiClient } from '@shodai-network/agreements-api-client'; const client = new ApiClient({ baseUrl: process.env.BASE_URL!, apiKey: process.env.API_KEY, }); const result = await client.validateTemplate(agreement); ``` ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -sS "$BASE_URL/v0/agreements/validate-template" \ -X POST \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ --data @agreement.json ``` The input is the authored agreement JSON itself. Do not wrap it in a deployment payload. ## Read the response The raw HTTP response is wrapped in a `data` envelope with request metadata. `client.validateTemplate(...)` unwraps `data` and returns the validation summary directly. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "data": { "templateId": "did:template:service-retainer-manual-balance-v0-1", "participantVariableKeys": [ "serviceProviderRepresentative", "clientRepresentative" ], "inputIds": [ "submitInitialPaymentProof", "submitInvoice", "submitInvoiceWithTopup" ], "stateIds": [ "AWAITING_PAYMENT", "WORK_IN_PROGRESS", "INVOICE_SUBMITTED" ], "warnings": [] }, "meta": { "apiVersion": "v0", "requestId": "req_123" } } ``` | Field | How to use it | | ------------------------- | -------------------------------------------------------------------------- | | `templateId` | Confirms the agreement metadata identity when one is present. | | `participantVariableKeys` | Confirms which participant-role variables the agreement exposes. | | `inputIds` | Confirms which authored business events can be submitted later. | | `stateIds` | Confirms which lifecycle states the agreement defines. | | `warnings` | Flags unusual or incomplete authoring choices to review before deployment. | ## What validation does not prove Passing `POST /v0/agreements/validate-template` does not prove that: 1. deployment values are present 2. participant wallet addresses have been supplied 3. observers are valid for your deployment context 4. signer or permit data is ready 5. the agreement has been deployed 6. every future input will be valid from every live state Use `POST /v0/agreements/validate` during [deployment](/workflow/deploy-an-agreement) to preflight the assembled deployment request. That deployment preflight does not deploy and does not validate permit signatures. ## Use the result as authoring feedback If participant keys are missing, inspect participant variables and `subtype: "participant"`. If input IDs are missing, inspect `execution.inputs`. If state IDs are unexpected, inspect `execution.states` and transitions. If warnings appear, resolve or consciously accept them before signing deployment data. ## Next step When participant keys, state IDs, input IDs, and warnings match your intent, continue to [Deploy an Agreement](/workflow/deploy-an-agreement).