# Introduction Welcome to the Truss API documentation. ## Overview This documentation provides guides and API references for integrating with Truss: - **Foundation API:** Manage resources such as [payment intents](./resources/payment-intents). - **Corex API:** Read authorized account data, transactions, and deposit statements through Truss's Core Exchange 6.3.1 / FDX integration. Start with the [Corex integration guide](./resources/corex). Corex access requires enablement by Truss for your integration and environment. The **Corex API Reference** documents the supported account data endpoints separately from the Foundation **API References**. ## Getting Started & Next Steps To get started you'll need to: 1. Review the [authentication requirements](./authentication/overview) to learn how to authenticate your requests 2. Choose the [Foundation payment intents guide](./resources/payment-intents) or the [Corex integration guide](./resources/corex) 3. Check out API guidelines and [references](/api-references) to understand how to make requests to our endpoints # Authentication This guide explains how to authenticate your requests to the Truss API. ## Overview Truss API endpoints use **Bearer token** authentication. All API requests require a valid JWT access token in the `Authorization` header. Access tokens are obtained through the OAuth 2.0 Authorization Code flow on the Truss Auth server. The Foundation and Corex APIs use this authentication flow with different resource scopes. Every Corex endpoint requires `accounts:read`; ask Truss to enable Corex access and approve that scope for your application. See the [Corex integration guide](../resources/corex) for requests and supported behavior. ### How It Works 1. **Obtain tokens** via the [OAuth 2.0 flow](./oauth-integration-guide) — your application redirects users to Truss for authorization, then exchanges the authorization code for an access token and refresh token at the Auth server. 2. **Call API endpoints** with the access token as a Bearer token. 3. **Refresh tokens** when the access token expires (every 60 minutes) by calling the token endpoint with your refresh token. ### Using Your Access Token Include the access token in the `Authorization` header when making API requests: ```http Authorization: Bearer {access_token} ``` :::scalar-callout{type="info" icon="clock"} **Token Expiry:** Access tokens expire after 60 minutes. Use your refresh token to obtain a new access token before expiry. ::: ### Getting Started For complete details on implementing authentication with Truss, including: - Prerequisites and obtaining OAuth credentials - Step-by-step OAuth flow implementation - Token exchange and refresh procedures - Available scopes and permissions - Testing in development or sandbox (Corex testing uses development) **See the complete [OAuth Integration Guide](./oauth-integration-guide).** # Truss OAuth Integration Guide This guide provides step-by-step instructions for developers looking to integrate with Truss using OAuth 2.0 authentication. With Truss OAuth, your users can securely authorize your application to access their Truss data without sharing their credentials. ## Prerequisites Before you begin integration, ensure that: 1. **Your users are already onboarded with Truss** - OAuth can only be used by users who have completed Truss onboarding 2. **You have obtained OAuth credentials from Truss** - Contact Truss directly to request: - `client_id` - Your unique application identifier - `client_secret` - Your confidential application secret (keep this secure!) - Approved `redirect_uri` - The callback URL where users will be redirected after authorization :::scalar-callout{type="warning"} **Important:** Keep your `client_secret` secure and never expose it in client-side code, version control, or logs. ::: ## Getting Started ### Base URLs Choose the URLs for your integration's environment. Development OAuth is supported for both Foundation and Corex; Corex integration testing currently uses development. | Environment | Base Auth URL | Base App URL | | --- | --- | --- | | Development | `https://auth.foundation.dev.trusspayments.com` | `https://app.dev.trusspayments.com` | | Sandbox | `https://auth.foundation.sandbox.trusspayments.com` | `https://app.staging.trusspayments.com` | | Production | `https://auth.foundation.trusspayments.com` | `https://app.trusspayments.com` | Use the **Base App URL** to redirect users to begin the OAuth flow. Use the **Base Auth URL** to complete the authorization code exchange and to refresh tokens. Use credentials, users, and companies from the same environment throughout the flow, and call the corresponding [Foundation or Corex API base URL](../api-guidelines/environments). Sandbox uses the staging app; it is separate from development. :::scalar-callout{type="info"} **Note:** Development and sandbox test users and companies must be signed up and onboarded as they would in production. Contact Truss for access and OAuth credentials for your chosen environment, then follow its base app URL to sign up. ::: ## OAuth Flow Overview Truss implements the OAuth 2.0 Authorization Code flow, which consists of: 1. **Authorization Request** - Redirect the user to Truss for authentication 2. **Authorization Grant** - User authorizes your application 3. **Token Exchange** - Exchange the authorization code for access and refresh tokens 4. **API Access** - Use the access token to make authenticated API requests 5. **Token Refresh** - Use the refresh token to obtain new access tokens when they expire ``` ┌─────────────┐ ┌─────────────┐ │ │ 1. Redirect to Authorization URL │ │ │ Your App │──────────────────────────────────────────>│ Truss │ │ │ │ │ │ │ 2. User Authorizes & Redirect with Code │ │ │ │<──────────────────────────────────────────│ │ │ │ │ │ │ │ 3. Exchange Code for Tokens │ │ │ │──────────────────────────────────────────>│ Token │ │ │ │ Endpoint │ │ │ 4. Return Access & Refresh Tokens │ │ │ │<──────────────────────────────────────────│ │ │ │ │ │ │ │ 5. API Request with Access Token │ │ │ │──────────────────────────────────────────>│ Truss API │ └─────────────┘ └─────────────┘ ``` ## Step 1: Initiate OAuth Authorization To begin the OAuth flow, redirect the user to Truss's authorization endpoint with the required parameters. ### Authorization Endpoint **Production:** ``` GET https://app.trusspayments.com/oauth/authorize ``` **Sandbox:** ``` GET https://app.staging.trusspayments.com/oauth/authorize ``` **Development:** ``` GET https://app.dev.trusspayments.com/oauth/authorize ``` ### Required Parameters | Parameter | Description | Example | | --------------- | ----------------------------------------------- | ------------------------------------------------------------------------- | | `client_id` | Your application's client ID | `your-client-id` | | `redirect_uri` | Your registered callback URL | `https://yourapp.com/oauth/callback` | | `response_type` | Must be `code` for authorization code flow | `code` | | `scope` | Space-separated list of requested scopes | `payment_intent.inbound:write transactions:read offline_access` | | `state` | Random string for CSRF protection (recommended) | `random-state-string` | ### Example Authorization URL **Production:** ``` https://app.trusspayments.com/oauth/authorize ?client_id=your-client-id &redirect_uri=https://yourapp.com/oauth/callback &response_type=code &scope=payment_intent.inbound:write+transactions:read+offline_access &state=random-state-string ``` **Sandbox:** ``` https://app.staging.trusspayments.com/oauth/authorize ?client_id=your-client-id &redirect_uri=https://yourapp.com/oauth/callback &response_type=code &scope=payment_intent.inbound:write+transactions:read+offline_access &state=random-state-string ``` **Development:** ``` https://app.dev.trusspayments.com/oauth/authorize ?client_id=your-client-id &redirect_uri=https://yourapp.com/oauth/callback &response_type=code &scope=payment_intent.inbound:write+transactions:read+offline_access &state=random-state-string ``` Request every scope required by the endpoints you call; see [Scopes by Endpoint](./scopes#scopes-by-endpoint). For example, an integration that creates bills and lets users choose the funding wallet requests `payment_intent.outbound:write transactions:read wallets:read offline_access`. For Corex testing, use the development authorization URL with `scope=accounts:read+offline_access`. See the [Corex integration guide](../resources/corex) for API requests. ## Step 2: Handle Authorization Callback After the user authorizes your application, Truss will redirect them back to your `redirect_uri` with an authorization code. ### Callback Parameters If successful, your callback URL will receive: | Parameter | Description | | --------- | --------------------------------------------------------- | | `code` | The authorization code (short-lived, single-use) | | `state` | The state parameter you provided (validate this matches!) | ## Step 3: Exchange Authorization Code for Tokens Once you have the authorization code, exchange it for access and refresh tokens by making a server-side POST request to the token endpoint. :::scalar-callout{type="warning"} **Security Note:** This step MUST be performed on your backend server, never in client-side code, as it requires your `client_secret`. ::: :::scalar-callout{type="info"} Truss partners with [Stytch](https://stytch.com/) to provide secure, enterprise-grade authentication. ::: ### Token Endpoint **Production:** `https://auth.foundation.trusspayments.com/v1/oauth2/token` **Sandbox:** `https://auth.foundation.sandbox.trusspayments.com/v1/oauth2/token` **Development:** `https://auth.foundation.dev.trusspayments.com/v1/oauth2/token` Make a `POST` request with `Content-Type: application/x-www-form-urlencoded` and these parameters: - `grant_type=authorization_code` - `code` (the authorization code from callback) - `redirect_uri` (must match the URI from Step 1) - `client_id` (your client ID) - `client_secret` (your client secret) **Response:** Returns `access_token`, `refresh_token`, `token_type`, `expires_in`, and `scope`. **Documentation (via Stytch):** [Exchange Authorization Code for Access Token](https://stytch.com/docs/b2b/api/connected-app-token) You can also try this endpoint directly from the [API Reference](/api-references/tag/authentication/post/v1/oauth2/token). ### Using the Access Token Include the access token in the `Authorization` header when making API requests to Truss: ```http Authorization: Bearer {access_token} ``` :::scalar-callout{type="info" icon="clock"} **Token Expiry:** Access tokens expire after 60 minutes. Use the refresh token to obtain a new access token before expiry (see Step 4). ::: ## Step 4: Refreshing Access Tokens Access tokens expire after 60 minutes. Use your refresh token to obtain a new access token without requiring the user to re-authorize. ### Token Refresh Endpoint **Production:** `https://auth.foundation.trusspayments.com/v1/oauth2/token` **Sandbox:** `https://auth.foundation.sandbox.trusspayments.com/v1/oauth2/token` **Development:** `https://auth.foundation.dev.trusspayments.com/v1/oauth2/token` Make a `POST` request with `Content-Type: application/x-www-form-urlencoded` and these parameters: - `grant_type=refresh_token` - `refresh_token` (your current refresh token) - `client_id` (your client ID) - `client_secret` (your client secret) **Response:** Returns a new `access_token` and potentially a new `refresh_token` (depending on your client type and token rotation settings). :::scalar-callout{type="warning"} **Important:** Always check for a new `refresh_token` in the response and update your stored token accordingly. ::: :::scalar-callout{type="info" icon="clock"} **Refresh Token Expiry:** The refresh token has an initial expiry of 6 months, and each use extends its lifetime by another 3 months. ::: **Documentation (via Stytch):** [Exchange Refresh Token for Access Token](https://stytch.com/docs/b2b/api/connected-app-token-refresh-token) ## Scopes For detailed information about available OAuth scopes and their permissions, see the [Scopes documentation](./scopes). ## Testing Your Integration ### Choose a Test Environment Test your integration in development or sandbox before going to production, using the environment agreed with Truss. Corex testing currently uses development. Keep authorization, token exchange, token refresh, and API requests in that environment; sandbox credentials and test accounts cannot be used in development. --- **Last Updated:** October 2026 **Version:** 1.0 # OAuth Scopes Scopes define what permissions your application is requesting from the user. Request only the scopes your application needs. ## Available Scopes | Scope | Description | Recommended | |-------|-------------|-------------| | `offline_access` | Grants a refresh token for offline access | ✅ Yes | | `payment_intent.inbound:read` | Read access to inbound payment intents (invoices) | As needed | | `payment_intent.inbound:write` | Write access to inbound payment intents (invoices) | As needed | | `payment_intent.outbound:read` | Read access to outbound payment intents (bills) | As needed | | `payment_intent.outbound:write` | Write access to outbound payment intents (bills) | As needed | | `contacts:read` | Read access to contacts | As needed | | `contacts:write` | Write access to contacts | As needed | | `transactions:read` | Read access to transaction history. Required alongside the payment intent scopes for every payment intent endpoint except list | As needed | | `wallets:read` | Read access to the company's wallets (name, last four digits and whether the wallet is primary) | As needed | | `company:read` | Read access to non-sensitive company info | As needed | | `accounts:read` | Read Corex accounts, transactions, customer and account contact information, payment networks, and deposit statements | Required for Corex | Every [Corex endpoint](../resources/corex) requires `accounts:read`, including transactions and customer details. The Foundation `transactions:read`, `contacts:read`, and `company:read` scopes do not replace it. Ask Truss to enable Corex and approve `accounts:read` for your application before requesting it through OAuth. ## Scopes by Endpoint A request must carry every scope listed for its endpoint. Inbound and outbound scopes are separate: a token with only inbound scopes cannot call outbound endpoints, and the reverse. | Endpoint | Required scopes | | --- | --- | | `GET /v1/company/` | `company:read` | | `GET /v1/contacts/`, `GET /v1/contacts/{uid}/` | `contacts:read` | | `PATCH /v1/contacts/{uid}/` | `contacts:write` | | `GET /v1/wallets/` | `wallets:read` | | `GET /v1/payment-intents/inbound/` | `payment_intent.inbound:read` | | `GET /v1/payment-intents/inbound/{uid}/` | `payment_intent.inbound:read` `transactions:read` | | `POST /v1/payment-intents/inbound/`, `PATCH /v1/payment-intents/inbound/{uid}/`, `POST /v1/payment-intents/inbound/{uid}/cancel/` | `payment_intent.inbound:write` `transactions:read` | | `GET /v1/payment-intents/outbound/` | `payment_intent.outbound:read` | | `GET /v1/payment-intents/outbound/{uid}/` | `payment_intent.outbound:read` `transactions:read` | | `POST /v1/payment-intents/outbound/`, `PATCH /v1/payment-intents/outbound/{uid}/`, `POST /v1/payment-intents/outbound/{uid}/cancel/` | `payment_intent.outbound:write` `transactions:read` | `wallets:read` is only needed to list wallets. Choosing a wallet with `wallet_uid` on create does not require `wallets:read`, but still requires the payment intent write scope and `transactions:read` (see the create rows in the table above). Requests without a required scope return `403`. # Environments The Truss API provides different environments to support development, testing, and production use cases. ## Available Environments | Environment | Base API URL | Base Auth URL | | --- | --- | --- | | Development | `https://api.foundation.dev.trusspayments.com` | `https://auth.foundation.dev.trusspayments.com` | | Sandbox | `https://api.foundation.sandbox.trusspayments.com` | `https://auth.foundation.sandbox.trusspayments.com` | | Production | `https://api.foundation.trusspayments.com` | `https://auth.foundation.trusspayments.com` | Use the **Base API URL** for Foundation resource endpoints (payment intents, company, etc.). Use the **Base Auth URL** for token exchange and refresh endpoints (`/v1/oauth2/token`). ### Corex Base URLs Corex uses a separate API host from Foundation, with the `/fdx/v6` base path: | Environment | Corex Base URL | Base Auth URL | | --- | --- | --- | | Development | `https://api.dev.trusspayments.com/fdx/v6` | `https://auth.foundation.dev.trusspayments.com` | | Production | `https://api.trusspayments.com/fdx/v6` | `https://auth.foundation.trusspayments.com` | Contact Truss to confirm Corex access and routing for your integration in each environment before making requests. Listing these URLs does not indicate that Corex is enabled for your application. Corex requires the `accounts:read` scope; use the matching environment's credentials and Auth URL from this table. See the [Corex integration guide](../resources/corex). ### Development Environment Development supports OAuth integration testing for both Foundation and Corex. Corex testing currently uses this environment. Authorize users at `https://app.dev.trusspayments.com/oauth/authorize`, then exchange and refresh tokens at `https://auth.foundation.dev.trusspayments.com/v1/oauth2/token`. Use the development API URL for the API you are calling from the tables above. Contact Truss for development access, OAuth credentials, and test account setup. Development is separate from sandbox: use development credentials and onboard users and companies at `https://app.dev.trusspayments.com`. See the [OAuth Integration Guide](../authentication/oauth-integration-guide) for the full flow. ### Sandbox Environment The sandbox environment is designed for integration testing before going live. **Base API URL:** `https://api.foundation.sandbox.trusspayments.com` **Base Auth URL:** `https://auth.foundation.sandbox.trusspayments.com` **Characteristics:** - Test credentials and test data only - No real payment processing - Rate limits may differ from production (see [Rate Limiting](./rate-limiting)) :::scalar-callout{type="info"} The sandbox should not be used for integrations in production environments. ::: ### Production Environment The production environment is for live transactions and real-world usage. **Base API URL:** `https://api.foundation.trusspayments.com` **Base Auth URL:** `https://auth.foundation.trusspayments.com` **Characteristics:** - Real payment processing - Production-grade rate limits and SLA ## Authentication Each environment requires separate API credentials. Credentials from one environment cannot be used in another environment. Token exchange and refresh requests must be sent to the environment's **Auth URL**. Resource requests use the **Base API URL** for Foundation or the **Corex Base URL** for Corex. See [authentication requirements](../authentication/overview) for a guide on how to authenticate. # Rate Limiting The Truss API implements rate limiting to ensure fair usage and maintain service stability for all users. ## Rate Limit Details All API endpoints are subject to rate limiting based on your API credentials. When you exceed the rate limit, the API will return a `429 Too Many Requests` response. The body of the response will indicate when the endpoint will become available. The following are the general rate limits for endpoints: | Environment | Limit | | --- | --- | | sandbox | 600 requests/minute, 10000 requests/day | | production | 120 requests/minute, 3000 requests/day | We recommend implementing exponential backoff to retry after `429` responses. ### Example Error Response ```json { "detail": "Request was throttled. Expected available in 15 seconds.", "error_code": "throttled" } ``` # Correlation IDs Correlation IDs (also known as Request IDs) are unique identifiers assigned to each API request. They help track requests through the system and are essential for debugging and support. ## How Correlation IDs Work Every API request and response includes a correlation ID in the header `Correlation-ID`. This ID uniquely identifies the request and can be used to trace the request's path through our systems. ### Request Header You can provide your own correlation ID by including it in the request header: ``` Correlation-ID: your-unique-identifier ``` The provided ID must be a valid GUID. If you don't provide a correlation ID or the given ID is not valid, the API will automatically generate one for you. The header is case-insensitive. ### Response Header Every API response includes the correlation ID in the response headers: ``` Correlation-ID: 550e8400e29b41d4a716446655440000 ``` ## Getting Support When contacting our support team via the shared Slack channel about an API issue, include the correlation ID from the response headers. This allows us to quickly locate your request in our logs and provide faster assistance. # Creating a sandbox test account Use the Truss **sandbox web application** to register a company and complete onboarding so you can try dashboard flows, link sandbox bank accounts, and exercise the API against [sandbox API and auth endpoints](./api-guidelines/environments#sandbox-environment). The sandbox app uses the same hostname as the payer-facing Payment Portal in non-production; see [Partner Payment Portal](./partner-payment-portal) for deep-link URLs. ## Open the sandbox app Go to: ```text https://app.staging.trusspayments.com ``` :::scalar-callout{type="info"} **Cloudflare Access.** The sandbox app sits behind Cloudflare Access for non-production systems. Contact Truss for access to the sandbox environment. ::: ## Sign up 1. On the Truss app, choose **Sign up** to start a new account. 2. Enter an **email** you control and a **new password**, then continue. 3. **Verify your email** using the one-time code sent to that address (or follow the in-app verification step your environment provides). ## Complete the onboarding application After email verification, the app walks you through the **business onboarding application** (company details, identity verification steps, and related prompts). - Ensure you select **Construction** as the industry. - When verifying your identity (via Plaid), use the Sandbox Mode test data provided. Because this is **sandbox**, do **not** enter real people’s personal data, real government IDs, or live financial details. Treat it as test data, with one practical exception: - **Email** — Use an address you can access for verification and testing. Fill each step from a *testing* perspective (for example a fictitious legal business name and test-appropriate inputs where the form allows). Some steps may offer **Skip** or optional fields; skipping these fields are encouraged in sandbox for faster application completion. ## Submit and approval When the application is complete, **submit** it. In **sandbox**, submitted applications are generally **approved automatically** or on a fast test path so you can reach the dashboard and continue setup (such as creating inbound payment intents in-app). If your account remains pending longer than expected, contact Truss support with the email you used to sign up. ## Accessing the test account On application approval and on every subsequent login using the email and password entered on sign up, you will be prompted to **verify your phone**. Enter the code `000001` to complete the verification. No SMS will be sent. ## Additional users Additional users can be added within Truss via the **Team** page. Because this is **sandbox**, do **not** enter real people’s personal data, with two practical exceptions: - **Email** — Use an address you can access for verification and testing. - **Phone** — Use a number that can receive **SMS** for two factor authentication. ## Related documentation - [Environments](./api-guidelines/environments) — Sandbox API and OAuth base URLs for API calls. - [OAuth Integration Guide](./authentication/oauth-integration-guide) — Connecting your integration to sandbox auth. - [Partner Payment Portal](./partner-payment-portal) — Staging hostname and payer deep links for invoice checkout testing. # Partner Payment Portal This guide describes how **integration partners** can send payers from their own product into the Truss **Payment Portal** checkout, straight to a specific **inbound payment intent** (invoice), and optionally return them to the partner when checkout finishes. For how companies configure and share the Payment Portal in general, see [How does the Payment Portal work?](https://articles.trusspayments.com/en/articles/7125377-how-does-the-payment-portal-work). For creating and managing intents via API, see [Payment intents](./resources/payment-intents). ## What is the Payment Portal? The Payment Portal is the Truss-hosted experience where a payer authenticates (email verification), sees the invoices they can pay for a given company, chooses a payment method, and completes payment. Each Truss company has a **checkout link** embedded in their portal URL path. ## Choose the checkout flow when creating the invoice **Always include `contact` when creating an inbound payment intent if you have an existing Truss contact or the customer's name and email.** Truss assigns that customer to the invoice, and the customer can verify their email in the Payment Portal to access the invoice, use available saved payment methods, and save supported methods for future payments. Omit `contact` only as a fallback when customer contact information is unavailable. This creates an invoice without an assigned customer in Truss and returns a `checkout_url` for guest checkout. While paying as a guest, the payer must enter payment details and cannot use saved payment methods or save methods for future payments. Your integration can use both flows, selecting one for each invoice based on the customer information available. See [Choosing between a contact and contactless checkout](./resources/payment-intents#choosing-between-a-contact-and-contactless-checkout) for a comparison and create examples, or [Customer exists in your product but is missing on the Truss invoice](./resources/payment-intents#customer-exists-in-your-product-but-is-missing-on-the-truss-invoice) for the integration change to make. ## Build a Payment Portal link Integration partners usually need two pieces of information to build a payer-facing link: - **Checkout link ID** — Identifier in the portal path (same ID that appears in the company’s standard Payment Portal URL as `/pay//...`). You can read it from the `checkout_link_id` field returned by [Get company](https://docs.trusspayments.com/api-references/tag/company/get/v1/company/) (`GET /v1/company/`). - **Payment intent UID** — The `uid` of the inbound payment intent from the [Payment Intent](https://docs.trusspayments.com/api-references/model/paymentintent) API resource (and the value you use elsewhere in API calls). Use the payer-facing Payment Portal hosts below—they match the **OAuth authorize** origins documented in [OAuth Integration Guide](./authentication/oauth-integration-guide). ## Deep link to a specific invoice Use one of these **base URLs**: **Production:** ``` https://app.trusspayments.com ``` **Sandbox:** ``` https://app.staging.trusspayments.com ``` Send the payer to: **Production:** ```http GET https://app.trusspayments.com/pay/{checkout_link_id}/paymentIntent/{payment_intent_uid} ``` **Sandbox:** ```http GET https://app.staging.trusspayments.com/pay/{checkout_link_id}/paymentIntent/{payment_intent_uid} ``` Example (production, illustrative identifiers): ```text https://app.trusspayments.com/pay/a1b2c3d4/paymentIntent/3dba9d05-bb20-423e-924e-4b19e50d6f3c ``` The app immediately routes them into the invoice detail step for that UID (query parameters on this URL, such as `redirect_uri`, are preserved). ### Email address The payer must complete the portal’s **email verification** using an email address registered as the primary or secondary contact email on the payment intent (see [Payment intents — Contacts](./resources/payment-intents#contacts)). If they use a different email, they may not see or open the correct invoice after signing in. :::scalar-callout{type="info"} Contactless inbound payment intents have no contact email. Send payers to the `checkout_url` returned by the API instead; see [Contactless inbound payment intents](./resources/payment-intents#contactless-inbound-payment-intents). ::: ## Optional return URL (`redirect_uri`) ### What it is for If you pass a validated `redirect_uri`, the portal can **send the payer back to your application** after a successful invoice payment (primary action on the receipt step), or when certain error paths offer “Close this page” action that retaurns early. Your server or SPA can read the query parameters Truss appends and reconcile state in your product. ### Query parameter Append a **single** absolute HTTPS URL (fully qualified), URL-encoded as one query parameter: ```text ?redirect_uri=https%3A%2F%2Fwww.google.com ``` You may combine it with the deep link routes above, for example (production): ```text https://app.trusspayments.com/pay/{checkout_link_id}/paymentIntent/{payment_intent_uid}?redirect_uri=https%3A%2F%2Fwww.google.com ``` ### When it is accepted - The value must be a **valid URL**. - For **cross-origin** callbacks, the **origin and path** must match an entry on Truss’s **allowlist** for the deployed web app (see below). Matching is **exact** for the pathname (no extra path segments): for example, if allowlisted as `https://partner.example.com/oauth/truss/callback`, then `https://partner.example.com/oauth/truss/callback/extra` will **not** match. - **Trailing slashes** are normalized when comparing to the allowlist. If `redirect_uri` is **present but not allowed**, the payer sees an **invalid link** error and cannot continue into checkout until they use a URL **without** `redirect_uri`, or with an allowlisted `redirect_uri`. If you **omit** `redirect_uri`, after payment, the payer remains in the portal (default **Done** navigates back to the payer’s invoice list for that company). ### Query parameters on redirect When a browser redirect to your `redirect_uri` is performed, Truss **sets** these query parameters on your URL: | Parameter | Description | | --- | --- | | `payment_intent_uid` | The payment intent UID for the checkout session (URL-encoded as needed). | | `result` | Outcome indicator: `success`, `incomplete`, or `fail`. | Any query string you already placed on your `redirect_uri` is **preserved**; Truss appends these parameters in addition to them. **`result` values (integration-relevant):** - **`success`** — Used when the payer completes an **invoice** (non–custom-amount) payment and confirms the primary completion action on the **receipt** step. This is the main “payment completed” signal for deep-linked invoice checkout. - **`incomplete`** — Used when the payer is returned early from certain error/closure flows (for example, closing the page via the offered action when checkout cannot proceed as linked). The `fail` value exists in the checkout implementation for symmetry; partner flows should not rely on seeing it until Truss documents a specific user-facing path that sets it. For robust reconciliation, combine `result` with [webhooks](./webhooks/setup-guide) and/or API polls on the payment intent and transactions. :::scalar-callout{type="info"} **Custom / non-invoice payments** inside the portal do not drive the partner return flow on receipt; the payer gets the normal in-portal completion behavior instead. ::: ### Allowlisting redirect URIs Cross-origin `redirect_uri` values **must be allowlisted** in Truss’s configuration for the Payment Portal web application. Contact **Truss** with the exact HTTPS URLs (scheme, host, and path) you need for sandbox and production. Until a URI is allowlisted, do not ship it to end users—it will block checkout when supplied. ## Practical checklist for partners 1. Obtain **checkout link ID** via [Get company](https://docs.trusspayments.com/api-references/tag/company/get/v1/company/) and use the correct **portal base URL** for the environment (`https://app.trusspayments.com` or `https://app.staging.trusspayments.com`). 2. Store the **`uid`** of each inbound payment intent you expose in your UX. 3. Build: `https://app.trusspayments.com/pay/{checkout_link_id}/paymentIntent/{uid}` or the sandbox hostname (+ optional encoded `redirect_uri`). 4. Ensure payers know which **email** to use for portal sign-in (same email the merchant notified). 5. Register and test every **`redirect_uri`** path with Truss before go-live. 6. On your return endpoint, parse `payment_intent_uid` and `result`; confirm payment state via API/webhooks rather than trusting `success` alone for financial reconciliation where your risk model requires it. # Corex The Truss Corex API provides account data using Core Exchange 6.3.1 / FDX response formats. It supports business checking accounts and, when available, the authorized customer's open business charge-card account. The **Corex API Reference** contains the full request and response schemas for the operations below. ## 1. Enable access and obtain an access token Contact Truss to obtain OAuth credentials and confirm Corex access in your chosen environment. Ask for the `accounts:read` scope to be approved for your application. Follow the [OAuth Integration Guide](../authentication/oauth-integration-guide), requesting `accounts:read`. Include `offline_access` if your integration needs refresh tokens: ```text scope=accounts:read offline_access ``` For the development environment used below, follow that OAuth flow with development credentials. Authorize users at `https://app.dev.trusspayments.com/oauth/authorize` and exchange or refresh tokens at `https://auth.foundation.dev.trusspayments.com/v1/oauth2/token`. Every Corex endpoint requires an access token with `accounts:read`, including customer details, transactions, and statements. Send it as a Bearer token. Foundation scopes such as `transactions:read` do not replace `accounts:read`. Use the [Corex base URL](../api-guidelines/environments#corex-base-urls) confirmed by Truss for your environment. The examples below use development: ```bash export TRUSS_COREX_BASE_URL='https://api.dev.trusspayments.com/fdx/v6' export TRUSS_ACCESS_TOKEN='YOUR_ACCESS_TOKEN' ``` Resource paths have no trailing slash. Include `/fdx/v6` only once when constructing requests. ## 2. List accounts and select an account ID ```bash curl --fail-with-body \ --header "Authorization: Bearer ${TRUSS_ACCESS_TOKEN}" \ "${TRUSS_COREX_BASE_URL}/accounts" ``` The response contains `accounts` and `page`. Each account has an `accountId`; copy the returned value for subsequent requests: ```bash export TRUSS_ACCOUNT_ID='ACCOUNT_ID_FROM_RESPONSE' ``` Treat account IDs as opaque identifiers. Do not substitute an account number, a card number, or a Foundation resource ID. Checking accounts have `accountCategory: DEPOSIT_ACCOUNT` and `accountType: CHECKING`. Charge-card accounts, when available, have `accountCategory: LOC_ACCOUNT` and `accountType: CHARGE`. Only accounts available to the authorized customer are returned. If no accounts are available, this endpoint returns HTTP 404. The account list returns all available accounts in one response with `page: {}`; it does not support pagination or account filters. ## 3. Fetch transactions and follow pagination ```bash curl --fail-with-body --get \ --header "Authorization: Bearer ${TRUSS_ACCESS_TOKEN}" \ --data-urlencode 'limit=100' \ --data-urlencode 'startTime=2026-01-01T00:00:00Z' \ --data-urlencode 'endTime=2026-01-31T23:59:59Z' \ "${TRUSS_COREX_BASE_URL}/accounts/${TRUSS_ACCOUNT_ID}/transactions" ``` The response contains a `transactions` array and a `page` object. Transactions are ordered newest first and reported with `status: POSTED`; pending card authorizations are not included. The supported query parameters are: | Parameter | Behavior | | --- | --- | | `endTime` | Optional inclusive end time. An omitted or future value is capped at the first request's current time. A date-only value includes that entire day, subject to the same cap. | | `limit` | Page size; defaults to `100`. Values are constrained to `1`–`500`. | | `offset` | For the next page, send the exact opaque value returned in `page.nextOffset`. Omit on the first request. | | `startTime` | Optional inclusive start time. Omit to read from the beginning of the available history. | Use ISO 8601 timestamps with a timezone, as shown above, or dates in `YYYY-MM-DD` format for the time filters. If `page.nextOffset` is present, copy it without decoding or modifying it and request the next page: ```bash export TRUSS_NEXT_OFFSET='NEXT_OFFSET_FROM_RESPONSE' curl --fail-with-body --get \ --header "Authorization: Bearer ${TRUSS_ACCESS_TOKEN}" \ --data-urlencode 'limit=100' \ --data-urlencode "offset=${TRUSS_NEXT_OFFSET}" \ "${TRUSS_COREX_BASE_URL}/accounts/${TRUSS_ACCOUNT_ID}/transactions" ``` Continue until `page.nextOffset` is absent. The cursor preserves the original time window; omit the time filters on subsequent requests. To use a different window, start a new request without an `offset`. ## 4. List and download deposit statements ```bash curl --fail-with-body \ --header "Authorization: Bearer ${TRUSS_ACCESS_TOKEN}" \ "${TRUSS_COREX_BASE_URL}/accounts/${TRUSS_ACCOUNT_ID}/statements" ``` Deposit statement lists contain closed monthly periods, newest first. The response contains `statements` and `page: {}` and does not support pagination or date filters. Choose a returned `statementId`: ```bash export TRUSS_STATEMENT_ID='STATEMENT_ID_FROM_RESPONSE' curl --fail \ --header "Authorization: Bearer ${TRUSS_ACCESS_TOKEN}" \ --output statement.pdf \ "${TRUSS_COREX_BASE_URL}/accounts/${TRUSS_ACCOUNT_ID}/statements/${TRUSS_STATEMENT_ID}" ``` A successful download returns `application/pdf`. Keep curl's default `Accept: */*` header for downloads; sending only `Accept: application/pdf` currently returns HTTP 406. A deposit PDF that is not yet available returns HTTP 404. Charge-card statement lists return an empty `statements` array, and charge-card statement downloads return HTTP 404. ## Supported operations All paths below are relative to the Corex base URL and require `accounts:read`. | Method | Path | Current support | | --- | --- | --- | | GET | `/accounts` | List available checking accounts and, when available, the charge-card account. | | GET | `/accounts/{accountId}` | Fetch one account. | | GET | `/accounts/{accountId}/asset-transfer-networks` | Returns `{"assetTransferNetworks": []}` for a valid account. Brokerage transfer networks are not supported. | | GET | `/accounts/{accountId}/contact` | Fetch account holder contact details; returns HTTP 404 when complete contact information is unavailable. | | GET | `/accounts/{accountId}/payment-networks` | Return `US_ACH` routing for a checking account when available. Charge-card accounts and checking accounts without routing return HTTP 404. | | GET | `/accounts/{accountId}/statements` | List closed deposit statement periods; charge-card lists are empty. | | GET | `/accounts/{accountId}/statements/{statementId}` | Download a deposit statement PDF. | | GET | `/accounts/{accountId}/transactions` | Read transactions using the parameters and pagination above. | | GET | `/customers/current` | Fetch the authorized customer's ID, name, and status. | Corex provides read access. Use only the operations and parameters documented in the Truss reference; other account types, filters, and operations in the broader Core Exchange specification are not supported by this integration. # Payment Intents [**Payment intents**](https://docs.trusspayments.com/api-references/model/paymentintent) represent an intent to collect payment from or send a payment to a contact. Truss has two kinds of payment intent: | Kind | `direction` | Contact | Wallet | Endpoints | | --- | --- | --- | --- | --- | | **Inbound payment intent** (invoice) | `0` | Customer who pays the company. Include whenever customer information is available. | Receiving wallet | `/v1/payment-intents/inbound/` | | **Outbound payment intent** (bill) | `1` | Vendor the company pays. Required. | Funding wallet | `/v1/payment-intents/outbound/` | Inbound and outbound payment intents share the same [PaymentIntent](https://docs.trusspayments.com/api-references/model/paymentintent) response shape but are created, listed and managed through separate endpoints and [scopes](../authentication/scopes). ## Contacts A payment intent may be attached to a [**contact**](https://docs.trusspayments.com/api-references/model/contact). A contact has a name and email associated to it, which is used to notify contacts to send or receive payment. An optional phone number may also be added. - On an **inbound** payment intent, the contact is the customer, sent as `contact` on create. **Always include `contact` when you have an existing Truss contact or the customer's name and email.** Omit it only as a fallback when customer contact information is unavailable; this creates a [contactless payment intent](#contactless-inbound-payment-intents). Contacts on inbound payment intents may have an optional secondary email with which a payment intent can be paid. - On an **outbound** payment intent, the contact is the vendor, sent as `vendor` on create. It is required. In both cases, provide the `uid` of an existing contact, or a `name` and `email`. A provided `uid` or a matching `name` uses and updates the existing contact; otherwise a new contact is created. :::scalar-callout{type="info"} In both sandbox and production contacts receive emails and SMS notifications related to the payment intent and resulting transactions. Ensure a real email address and SMS enabled phone number is used. ::: :::scalar-callout{type="info"} Existing contacts can be retrieved and updated via API. A contact's name, email, secondary email and phone number may be updated. See [API References](https://docs.trusspayments.com/api-references/tag/contacts) for more details. ::: ## Wallets A company may have more than one wallet (bank account) in Truss. Every payment intent is booked to one company wallet: - An **inbound** payment intent is booked to the **receiving wallet**, which receives the customer's payments. - An **outbound** payment intent is booked to the **funding wallet**, which pays the vendor. List the company's wallets with `GET /v1/wallets/` (requires `wallets:read`). Each wallet has a `uid`, an `is_primary` flag, a `name` and the last four digits of its account number (`mask`). To choose the wallet, send its `uid` as `wallet_uid` when creating the payment intent. If `wallet_uid` is omitted, the company's primary wallet is used. A `wallet_uid` that does not belong to the company returns a `422` validation error on `wallet_uid`. Payment intent responses include the booked wallet as a read-only `wallet_uid`. The wallet cannot be changed via API after the payment intent is created. ## Transactions A payment intent may have one or more related [**transactions**](https://docs.trusspayments.com/api-references/model/paymentintenttransaction). When a contact sends a payment or receives a payment, a transaction is created. Transactions either increment or decrement the `balance_paid` amount on their payment intent, depending their `direction`. For example, for an transaction on an inbound payment intent, a `direction` of `0` would increment the `balance_paid` while a `direction` of `1` would decrement the balance. In this scenario, the inbound-direction transaction is a payment made by the contact towards the payment intent while the outbound-direction transaction is likely a return or refund. ## Payment Intent Statuses & Lifecycle Payment intents progress through several statuses: | Status | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Pending` | The initial status of newly created payment intents awaiting payment | | `PartiallyPaid` | The payment intent has been partially paid (`balance_paid != total`) | | `Paid` | The payment intent has been fully paid (`balance_paid == total`) | | `Canceled` | The payment intent has been canceled | | `Draft` | (Outbound payment intents only) A payment on the bill has been submitted in Truss and is waiting for admin approval. This status is not achievable via API | | `Failed` | (Outbound payment intents only) The payment intent has a returned transaction (the payment has failed). This state is not achievable via API | | `Errored` | (Inbound payment intents only) The payment intent has an error related to the integrated accounting system. This state is not achievable via API | ### Inbound Payment Intents #### Creating an inbound payment intent In Truss, each company account is required to set up a [Payment Portal link](https://articles.trusspayments.com/en/articles/7125377-how-does-the-payment-portal-work) in order to create inbound payment intents and begin accepting payments from their customers. Once set up, inbound payment intents can be created by users via the Truss web client or mobile app, via an accounting integration, or via API. Newly created inbound payment intents will have a `Pending` status. Send `wallet_uid` to choose the receiving wallet; see [Wallets](#wallets). #### Choosing between a contact and contactless checkout The `contact` you send when creating the payment intent determines which customer is assigned to the invoice in Truss and which checkout experience they use. Customer information in your product is not automatically passed to Truss. | Behavior | With `contact` (recommended whenever available) | Without `contact` (contactless fallback) | | --- | --- | --- | | Customer on the invoice in Truss | The supplied customer is assigned. | No customer is assigned at creation. | | Checkout experience | The customer verifies their email and pays through the Payment Portal. | The payer starts guest checkout using the returned `checkout_url`. | | Saved payment methods | The verified customer can use their available saved payment methods and save supported methods for future payments. | While paying as a guest, the payer must enter payment details. They cannot use saved payment methods or save methods for future payments. | | `checkout_url` in the API response | `null`; use a [Payment Portal link](../partner-payment-portal). | A guest checkout link for this payment intent. | An integration can use both flows: include a contact for customers whose information you have, and use contactless creation only for those whose contact information is unavailable. #### Creating an inbound payment intent with a contact Send `POST /v1/payment-intents/inbound/` with an existing **Truss contact UID**: ```json { "total": "100.00", "addenda": "Invoice INV-1001", "contact": { "uid": "3dba9d05-bb20-423e-924e-4b19e50d6f3c" } } ``` The UID above is illustrative. Use a contact belonging to the company creating the invoice, not a customer ID from your own product. If you do not have a Truss contact UID, send the customer's **name and email** in the same create request: ```json { "total": "100.00", "addenda": "Invoice INV-1001", "contact": { "name": "Alex Customer", "email": "alex@example.com" } } ``` Truss uses and updates a matching contact or creates a new one; see [Contacts](#contacts). You do not need to create the contact in a separate API call. Send the customer to the [Payment Portal](../partner-payment-portal), where they verify the contact's primary or secondary email to access the invoice and their available saved payment methods. #### Accepting payment on an inbound payment intent A [**Payment Portal**](https://articles.trusspayments.com/en/articles/7125377-how-does-the-payment-portal-work) is a unique link generated for each Truss company. Companies are able to distribute this link via their preferred channel to give their customers a platform to view and pay payment intents which the company has sent to them to be paid. All `Pending`, `PartiallyPaid` and `Paid` inbound payment intents related to the customer are available to view in the Payment Portal. `Pending` and `PartiallyPaid` can be selected by customers to pay via one of the available payment methods. Once a customer pays, a transaction is created and the payment intent will move to a `PartiallyPaid` or `Paid` status, depending on if there is a remaining balance on the payment intent. :::scalar-callout{type="info"} Integration partners can open the Payment Portal directly on a payer’s inbound payment intent via a **deep link** and optionally return users to their own app. See [Partner Payment Portal](../partner-payment-portal) for more details. ::: #### Contactless inbound payment intents Use contactless creation only when customer contact information is unavailable. Send `POST /v1/payment-intents/inbound/` without `contact`: ```json { "total": "100.00", "addenda": "Invoice INV-1001" } ``` The invoice has no customer assigned in Truss at creation. The payer starts guest checkout and must enter their payment details; while paying as a guest, they cannot use saved payment methods or save methods for future payments. - No notifications are sent. `send_email` or `send_sms` set to `true` without a `contact` returns a `422` validation error. - The response includes a `checkout_url`: a guest checkout link for this payment intent. Share it with the payer through your own channel. - `checkout_url` is returned on create, retrieve, update and cancel responses. It is omitted from list responses and webhook payloads, and is `null` for payment intents that have a contact. Payment intents with a contact are paid through the Payment Portal instead; see [Partner Payment Portal](../partner-payment-portal). #### Customer exists in your product but is missing on the Truss invoice If your product has the customer's information but the invoice is created without a customer in Truss, check the request to `POST /v1/payment-intents/inbound/`. Omitting `contact` creates a contactless invoice even when that customer already exists in Truss. Truss does not infer the customer from the invoice description, memo, or customer information stored in your product. **To prevent this, include `contact` in the create request:** send the existing Truss contact UID, or the customer's name and email as shown above. Route the customer to the Payment Portal after creation. The contactless flow remains available as a fallback for other invoices whose customer information is unavailable. The Foundation API's update endpoint does not accept `contact`, so it cannot attach a customer to an already-created invoice. Include the contact at creation to avoid requiring a company user to assign the customer manually in Truss. #### Funds availability for payments Once a customer pays a Truss company the inbound transaction is subject to a settlement period while the funds arrive and clear in the Truss bank account. During this time, the funds are immediately available to spend to create outbound transactions in Truss or spend on Truss cards. Once sufficient inbound transactions are settled, the outgoing transactions complete processing and are sent to their destination. ### Outbound Payment Intents #### Creating an outbound payment intent Create a bill with `POST /v1/payment-intents/outbound/`: - `total`, `addenda` and `vendor` are required. `vendor` takes the `uid` of an existing vendor, or a `name` and `email`; see [Contacts](#contacts). - `note`, `memo` and `due_date` are optional. - `wallet_uid` is optional and chooses the funding wallet; see [Wallets](#wallets). A company cannot create a bill to pay itself. Newly created outbound payment intents have a `Pending` status. :::scalar-callout{type="info"} The Foundation API records the bill only. It does not create draft payments or pay the bill. A company user pays the bill in the Truss dashboard. ::: #### Paying a bill in the Truss dashboard After creating a bill, send a company user to the **Pay bill** form for it, using the `uid` from the create response: **Production:** ```text https://app.trusspayments.com/dashboard/bills?view=pay-bill&id=new&paymentIntentUid={uid} ``` **Sandbox:** ```text https://app.staging.trusspayments.com/dashboard/bills?view=pay-bill&id=new&paymentIntentUid={uid} ``` To let the user review the bill before paying, link to the bill detail on the same host instead: ```text /dashboard/bills?view=payment-intent&id={uid} ``` The user signs in to Truss as usual. The link is kept through login, two-factor verification and company selection. Opening it does not start a payment; the user chooses how to pay and confirms in the form. A user without access to the bill's company is not shown the bill or the payment form. :::scalar-callout{type="info"} These dashboard links are for **company users** paying their own bills. They are separate from the [Payment Portal](../partner-payment-portal), where **customers** pay a company's inbound payment intents. ::: #### Updating and canceling an outbound payment intent - `total`, `addenda` and `note` cannot be updated once a bill is partially paid, fully paid or canceled. `memo` and `due_date` can be updated at any time. - A bill cannot be canceled once it is fully paid or already canceled, or while it has a payment that has not been returned, refunded or canceled. ## Payment intent webhooks Truss sends [webhooks](../webhooks/setup-guide) when payment intents change. **Inbound and outbound payment intents use the same events and payload fields**; use `direction` on the payment intent in `event_payload` to tell invoices from bills. | `event_name` | When it fires | | --- | --- | | `payment_intent.paid` | The payment intent is fully paid (`balance_paid` equals `total`). | | `payment_intent.canceled` | The payment intent is canceled. | | `payment_intent.updated` | Fields such as `total`, `addenda`, `note`, `memo`, or `due_date` change. | | `payment_intent.transaction.added` | A transaction or external transaction is added to the payment intent (for example when a customer pays an invoice or a company pays a bill). | These events are sent for payment intents created through the Foundation API and for those created in the Truss dashboard or other Truss channels. Webhook payloads use the same payment intent fields as list responses: they include `wallet_uid` and omit `transactions`, except `payment_intent.transaction.added`, which includes the new `transaction` on the payload. `checkout_url` is omitted from webhook payloads. To reconcile a bill after dashboard payment, handle `payment_intent.transaction.added` and `payment_intent.paid` the same way you would for an inbound invoice. Event schemas are in the [API Reference](https://docs.trusspayments.com/api-references/tag/webhooks/). ## Managing Payment Intents Via API | Operation | Inbound | Outbound | | --- | --- | --- | | Create | `POST /v1/payment-intents/inbound/` | `POST /v1/payment-intents/outbound/` | | List | `GET /v1/payment-intents/inbound/` | `GET /v1/payment-intents/outbound/` | | Retrieve | `GET /v1/payment-intents/inbound/{uid}/` | `GET /v1/payment-intents/outbound/{uid}/` | | Update | `PATCH /v1/payment-intents/inbound/{uid}/` | `PATCH /v1/payment-intents/outbound/{uid}/` | | Cancel | `POST /v1/payment-intents/inbound/{uid}/cancel/` | `POST /v1/payment-intents/outbound/{uid}/cancel/` | List responses omit `transactions`; retrieve a payment intent to see them. See [Scopes](../authentication/scopes) for the scopes each endpoint requires, and the API References for [inbound](https://docs.trusspayments.com/api-references/tag/inbound-payment-intents) and [outbound](https://docs.trusspayments.com/api-references/tag/outbound-payment-intents) payment intents and [wallets](https://docs.trusspayments.com/api-references/tag/wallets) for request and response details. # Webhook Setup Guide Event types and payload schemas are documented in the [API Reference](https://docs.trusspayments.com/api-references/tag/webhooks/). Payment intent webhooks use the same `event_name` values and payload shape for **inbound invoices and outbound bills**. The `event_payload` includes the payment intent's `direction` (`0` inbound, `1` outbound) and read-only `wallet_uid`. Webhooks are delivered for payment intents created via the Foundation API and for those created inside Truss. See [Payment intent webhooks](../resources/payment-intents#payment-intent-webhooks) for when each event fires. ## Registration Contact Truss to register your webhook URL. Your endpoint must be HTTPS and publicly reachable. No self-service; we configure webhooks for you. ## Delivery Truss sends webhooks as `POST` requests with `Content-Type: application/json`. Deliveries are signed with `X-Webhook-Signature` so you can verify authenticity if a signing secret has been set up for your webhook subscriber. See [Webhook Signature Verification](./signature-verification). We allow 30 seconds per request and retry up to 7 times with exponential backoff. Retries occur on 5xx, 408, 429, and transient connection errors. ## Best Practices - Return HTTP 200 quickly; process the payload asynchronously. - Use `event_id` for idempotency (duplicate deliveries are possible). - Validate `event_name` before processing. # Webhook Signature Verification Each webhook delivery is signed with HMAC-SHA256 so you can verify that the request came from Truss and has not been replayed or tampered with. ## Obtaining Your Signing Secret When webhook signing is enabled for your subscriber, Truss assigns a **signing secret**: a 64-character hexadecimal string (32 random bytes) unique to your webhook endpoint. Contact Truss to retrieve your signing secret. There is no self-service endpoint for this today. If you need to rotate your secret, contact Truss and we will provision a new one. :::scalar-callout{type="warning"} **Important:** Treat your signing secret like a password. Store it in a secrets manager, never commit it to source control, and never log it. ::: ## The Signature Header When a signing secret is configured, every webhook delivery includes an `X-Webhook-Signature` header: ```text X-Webhook-Signature: t=1700000000,v1=a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 ``` | Component | Description | | --- | --- | | `t` | Unix timestamp (seconds) at the time Truss sent the webhook | | `v1` | HMAC-SHA256 signature as a lowercase hex string (64 characters) | The `v1` prefix is a version identifier. Future signature schemes may use additional version keys in the same header. If no signing secret is configured for your subscriber, the `X-Webhook-Signature` header is omitted entirely. ## What Is Signed The signature is computed over the UTF-8 bytes of: ```text {timestamp}.{raw_request_body} ``` Where `{timestamp}` is the value of `t` from the header and `{raw_request_body}` is the exact JSON string sent in the HTTP request body — not a re-serialized version of a parsed object. :::scalar-callout{type="warning"} **Important:** You must verify the signature against the **raw request body bytes** as received over the wire. Parsing JSON and re-serializing it (even with the same data) can change key order or spacing and will cause verification to fail. ::: ## Verifying a Signature 1. Read the raw request body before parsing JSON. 2. Read the `X-Webhook-Signature` header. Reject the request if it is missing. 3. Parse the header: split on `,`, then split each part on the first `=` to extract `t` and `v1`. 4. Reject the request if `|now - t|` exceeds your tolerance window (Truss recommends **300 seconds**). 5. Compute `HMAC_SHA256(signing_secret, "{t}." + raw_body)` and compare the resulting hex digest to `v1` using a constant-time comparison. 6. Only after verification succeeds, parse the JSON body and process the event. ## Examples Use this helper to verify the `X-Webhook-Signature` header: ```javascript const crypto = require('crypto'); const TOLERANCE_SECONDS = 300; // Assumes rawBody is the exact, unparsed request body as received from Truss. function verifyTrussSignature(rawBody, signatureHeader, signingSecret) { if (!signatureHeader) { return false; } const parts = Object.fromEntries( signatureHeader.split(',').map((part) => part.split('=', 2)) ); const timestamp = parseInt(parts.t, 10); const expectedSignature = parts.v1; if (!timestamp || !expectedSignature) { return false; } const now = Math.floor(Date.now() / 1000); if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) { return false; } const signedPayload = `${timestamp}.${rawBody}`; const computedSignature = crypto .createHmac('sha256', signingSecret) .update(signedPayload, 'utf-8') .digest('hex'); const expectedBuffer = Buffer.from(expectedSignature, 'utf-8'); const computedBuffer = Buffer.from(computedSignature, 'utf-8'); return ( expectedBuffer.length === computedBuffer.length && crypto.timingSafeEqual(expectedBuffer, computedBuffer) ); } ``` ```python import hmac import hashlib import time TOLERANCE_SECONDS = 300 # Assumes raw_body is the exact, unparsed request body bytes as received from Truss. def verify_truss_signature(raw_body: bytes, signature_header: str, signing_secret: str) -> bool: if not signature_header: return False parts = dict(part.split("=", 1) for part in signature_header.split(",")) timestamp = int(parts.get("t", 0)) expected_signature = parts.get("v1") if not timestamp or not expected_signature: return False if abs(int(time.time()) - timestamp) > TOLERANCE_SECONDS: return False body_str = raw_body.decode("utf-8") signed_payload = f"{timestamp}.{body_str}".encode("utf-8") computed_signature = hmac.new( signing_secret.encode("utf-8"), signed_payload, hashlib.sha256, ).hexdigest() return hmac.compare_digest(computed_signature, expected_signature) ``` ## Replay Protection The timestamp is included in the signed payload, so a signature from an old delivery cannot be reused with a different body. Combined with the tolerance check (reject requests where `t` is more than 5 minutes from your server's clock), this protects against replay attacks. Ensure your server's clock is reasonably accurate (NTP). If your tolerance is too large, replayed requests remain valid for longer. ## Best Practices - Always read the raw body before parsing JSON. - Use a constant-time comparison (`crypto.timingSafeEqual` in Node.js, `hmac.compare_digest` in Python). - Reject any request that lacks `X-Webhook-Signature` once signing is enabled for your subscriber. - Store the signing secret in your secrets manager; never log it or commit it to source control. - Use `event_id` for idempotency — duplicate deliveries are possible (see the [Webhook Setup Guide](./setup-guide)). For delivery behavior, retries, and endpoint requirements, see the [Webhook Setup Guide](./setup-guide).