docs: introduce versioned docs and migrate to fuma (#11166)

## Todos for release

- [x] Configure Env in docs project on vercel
- [x] Configure Root Path in the docs project on vercel
- [ ] Remove old CSP https://github.com/zitadel/website/pull/1592

## What we did

This pull request migrates the project documentation from the old
`docs/` directory to the new `apps/docs/` directory, introduces a new
documentation system built with Next.js and Fumadocs, and updates all
relevant references, configuration files, and documentation to reflect
this change. It also adds new configuration and ignore files for the new
documentation app, updates CI and linting to exclude the new docs from
certain checks, and revises the contributing guidelines accordingly.

**Documentation System Migration and New Docs App**

* Migrated all documentation from `docs/` to `apps/docs/`, and updated
all references in `README.md`, `CONTRIBUTING.md`, and other files to
point to the new location.
[[1]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L585-L640)
[[2]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L660-R607)
[[3]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L740-R687)
[[4]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L2-R3)
[[5]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L30-R30)
* Added a new Next.js/Fumadocs-based documentation app under
`apps/docs/`, including core app files, layouts, routing, search API,
and a comprehensive `README.md` with development and contribution
instructions.
[[1]](diffhunk://#diff-5a1b07344a2c1b4d3f37b23ff1388b62cd9f57dea3c1cd23d8a0412b7602b132R1-R74)
[[2]](diffhunk://#diff-462b9ad1eabbb7d1c29bb9c36e4931eb180fd7190900e6d2babf8f4d66ad1c28R1-R39)
[[3]](diffhunk://#diff-e16ae25660ded787b10ac35dea96d5ecaacf895dae0afc8a9bd4382dc79a8c87R1-R7)
[apps/docs/app/[[...slug]]/layout.tsxR1-R81](diffhunk://#diff-59e08acde4e805b7aeccef1dcf98f1d71dfc550777e6b9402085cee0e9fa4e0aR1-R81),
[apps/docs/app/[[...slug]]/page.tsxR1-R79](diffhunk://#diff-e5df3f80d0fa01e12d63d81f29c57a9d14e846c78fd3beabb2ef768e38fd9580R1-R79),
[[4]](diffhunk://#diff-389b34918e040cacaa87cd7201ffa462cc2b0b716736f537e3d3c660ac69353bR1-R7)
[[5]](diffhunk://#diff-d3b03416d1c457b19f1c27b26f2db741412df841b875c26ccadc36e4522247f4R1-R29)
[[6]](diffhunk://#diff-c8fb8339570a5305809be7c618e14705fd86390dc278e6ce8ba224a7bc8b0c3cR1-R25)

**Configuration and Tooling Updates**

* Updated `.github/workflows/codeql.yml`, `.golangci.yaml`, and
`.github/dependabot.yml` to properly handle the new docs app: excluded
`apps/docs` from certain checks, added npm dependency updates for the
docs app, and excluded generated content.
[[1]](diffhunk://#diff-12783128521e452af0cfac94b99b8d250413c516ec71fe6d97dbea666ff7ba27L8-R14)
[[2]](diffhunk://#diff-9917ddc9f1c3304218f7269265b746d997c5c0615478177b5fceecd33ef47cb5R5-R6)
[[3]](diffhunk://#diff-9917ddc9f1c3304218f7269265b746d997c5c0615478177b5fceecd33ef47cb5R126-R129)
[[4]](diffhunk://#diff-dd4fbda47e51f1e35defb9275a9cd9c212ecde0b870cba89ddaaae65c5f3cd28R89-R106)
* Updated `.devcontainer/devcontainer.json` to use the latest Go 1.25.3
version for consistency.

**Licensing and Miscellaneous**

* Added `apps/docs/` to the list of licensed directories in
`LICENSING.md`.

These changes ensure the documentation is now maintained in a modern,
scalable system and all project tooling is updated to support the new
structure.

---------

Co-authored-by: Federico Coppede <[email protected]>
This commit is contained in:
Florian Forster
2026-01-29 08:38:36 -08:00
committed by GitHub
co-authored by Federico Coppede
parent 7b6395c348
commit d8ea041c56
2826 changed files with 78739 additions and 23571 deletions
@@ -0,0 +1,41 @@
//go:build ignore
package main
import (
"fmt"
"io"
"net/http"
"github.com/zitadel/zitadel-go/v3/pkg/actions"
)
const signingKey = "somekey" // signing key received after creating the target
// webhook HandleFunc to read the request body and then print out the contents
func webhook(w http.ResponseWriter, req *http.Request) {
// read the body content
sentBody, err := io.ReadAll(req.Body)
if err != nil {
// if there was an error while reading the body return an error
http.Error(w, "error", http.StatusInternalServerError)
return
}
defer req.Body.Close()
// validate signature
if err := actions.ValidateRequestPayload(sentBody, &req.Header, signingKey); err != nil {
// if the signed content is not equal the sent content return an error
http.Error(w, "error", http.StatusInternalServerError)
return
}
// print out the read content
fmt.Println(string(sentBody))
}
func main() {
// handle the HTTP call under "/webhook"
http.HandleFunc("/webhook", webhook)
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
http.ListenAndServe(":8090", nil)
}
@@ -0,0 +1,109 @@
//go:build ignore
package main
import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"log"
"net/http"
"os"
"github.com/go-jose/go-jose/v4"
"github.com/zitadel/oidc/v3/pkg/client"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
// webhook HandleFunc to read the request body and then print out the contents
func webhook(signatureAlgorithms []jose.SignatureAlgorithm, keySet oidc.KeySet, privateKey any) func(w http.ResponseWriter, req *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
// read the body content
sentBody, err := io.ReadAll(req.Body)
if err != nil {
// if there was an error while reading the body return an error
http.Error(w, "error", http.StatusInternalServerError)
return
}
defer req.Body.Close()
// decrypt the JWE, validate the JWT and extract the payload from it
payload, err := validateJWE(req.Context(), string(sentBody), signatureAlgorithms, keySet, privateKey)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// print out the payload
fmt.Println(string(payload))
}
}
func validateJWE(ctx context.Context, jweString string, signatureAlgorithms []jose.SignatureAlgorithm, keySet oidc.KeySet, privateKey any) ([]byte, error) {
// For parsing the JWE, we need to specify which algorithms we expect.
// You can specify either `RSA_OAEP_256` (rsa.PrivateKey) or `ECDH_ES_A256KW` (ecdsa.PrivateKey) or both, depending on which private key(s) you have.
// The content encryption algorithm will always be `A256GCM` as used by Zitadel.
parsedJWE, err := jose.ParseEncrypted(jweString, []jose.KeyAlgorithm{jose.RSA_OAEP_256, jose.ECDH_ES_A256KW}, []jose.ContentEncryption{jose.A256GCM})
if err != nil {
return nil, err
}
// In this example we only use a single key and loaded it on start, but you can manage and rotate the key used for the encryption through the API.
// You might also have a key management system in your application.
// Using the KeyID from the JWE header (parsedJWE.Header.KeyID), we could load the correct private key for decryption.
// Decrypt the JWE using the private key to get the inner JWT
decryptedJWT, err := parsedJWE.Decrypt(privateKey)
if err != nil {
return nil, err
}
// Now validate the inner JWT and return the payload
return validateJWT(ctx, string(decryptedJWT), signatureAlgorithms, keySet)
}
func validateJWT(ctx context.Context, jwtString string, algorithms []jose.SignatureAlgorithm, keySet oidc.KeySet) ([]byte, error) { //nolint:typecheck
// Parse the signed JWT to get the JWS object (which contains the signatures and unverified payload)
parsedJWS, err := jose.ParseSigned(jwtString, algorithms)
if err != nil {
return nil, err
}
// Verify the signature using the retrieved key and return the payload
return keySet.VerifySignature(ctx, parsedJWS)
}
func main() {
ctx := context.Background()
// set the issuer to the ZITADEL instance URL
issuer := "http://localhost:8080"
// the oidc client library will call the discovery endpoint to get the JWKS URI and supported signing algorithms
discover, err := client.Discover(ctx, issuer, http.DefaultClient)
if err != nil {
log.Fatal(err)
}
signatureAlgorithms := make([]jose.SignatureAlgorithm, len(discover.IDTokenSigningAlgValuesSupported))
for i, alg := range discover.IDTokenSigningAlgValuesSupported {
signatureAlgorithms[i] = jose.SignatureAlgorithm(alg)
}
keySet := rp.NewRemoteKeySet(http.DefaultClient, discover.JwksURI)
// this example uses the private key from the PEM file to decrypt the JWE
// make sure to load your private key accordingly
privateKeyPEM, err := os.ReadFile("./private-key.pem")
if err != nil {
log.Fatal(err)
}
block, _ := pem.Decode(privateKeyPEM)
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
log.Fatal(err)
}
// handle the HTTP call under "/webhook"
http.HandleFunc("/webhook", webhook(signatureAlgorithms, keySet, privateKey))
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
http.ListenAndServe(":8090", nil)
}
@@ -0,0 +1,72 @@
//go:build ignore
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"github.com/go-jose/go-jose/v4"
"github.com/zitadel/oidc/v3/pkg/client"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
// webhook HandleFunc to read the request body and then print out the contents
func webhook(signatureAlgorithms []jose.SignatureAlgorithm, keySet oidc.KeySet) func(w http.ResponseWriter, req *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
// read the body content
sentBody, err := io.ReadAll(req.Body)
if err != nil {
// if there was an error while reading the body return an error
http.Error(w, "error", http.StatusInternalServerError)
return
}
defer req.Body.Close()
// validate the JWT and extract the payload from it
payload, err := validateJWT(req.Context(), string(sentBody), signatureAlgorithms, keySet)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// print out the payload
fmt.Println(string(payload))
}
}
func validateJWT(ctx context.Context, jwtString string, algorithms []jose.SignatureAlgorithm, keySet oidc.KeySet) ([]byte, error) { //nolint:typecheck
// Parse the signed JWT to get the JWS object (which contains the signatures and unverified payload)
parsedJWS, err := jose.ParseSigned(jwtString, algorithms)
if err != nil {
return nil, err
}
// Verify the signature using the retrieved key and return the payload
return keySet.VerifySignature(ctx, parsedJWS)
}
func main() {
ctx := context.Background()
// set the issuer to the ZITADEL instance URL
issuer := "http://localhost:8080"
// the oidc client library will call the discovery endpoint to get the JWKS URI and supported signing algorithms
discover, err := client.Discover(ctx, issuer, http.DefaultClient)
if err != nil {
log.Fatal(err)
}
signatureAlgorithms := make([]jose.SignatureAlgorithm, len(discover.IDTokenSigningAlgValuesSupported))
for i, alg := range discover.IDTokenSigningAlgValuesSupported {
signatureAlgorithms[i] = jose.SignatureAlgorithm(alg)
}
keySet := rp.NewRemoteKeySet(http.DefaultClient, discover.JwksURI)
// handle the HTTP call under "/webhook"
http.HandleFunc("/webhook", webhook(signatureAlgorithms, keySet))
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
http.ListenAndServe(":8090", nil)
}
@@ -0,0 +1,133 @@
---
title: Migrate from Actions V1 to V2
sidebar_label: Migrate from V1 to V2
---
This guide helps existing users understand the shift from Actions V1 (Embedded) to V2 (Webhook), and provides guidance for new users on how to work with Actions V2.
<Callout type="caution">
Deprecation Notice
Actions V1 APIs are planned to be sunsetted in ZITADEL V5. While a definitive V5 release date is not yet available, this is the final timeline for V1 API removal. Actions V1 will receive no new features, and all new implementations must use V2.
</Callout>
## Why Actions Changed from V1 to V2
ZITADEL Actions evolved from V1 to V2 to address fundamental architectural limitations. While Actions V1 executed JavaScript code directly within ZITADEL's runtime, Actions V2 uses external HTTP endpoints.
This shift enables:
- **Better Scalability:** Distributed, serverless execution
- **Greater Flexibility:** Support for any technology stack (not just JavaScript)
- **Improved Resilience:** Isolating custom logic from core ZITADEL services
- **Cost Optimization:** Pay-per-execution models with serverless providers
- **Enhanced Monitoring:** Better debugging and observability of custom extensions
The transition represents a move from "embedded extensions" to "true webhooks," aligning ZITADEL with industry patterns (GitHub Actions, Zapier, etc.).
## Architecture Deep Dive: V1 vs V2
### Actions V1: The Embedded Model
Custom JavaScript code executed directly inside ZITADEL's process. For a complete list of V1 flow types, see [Actions V1 Flow Types](/guides/manage/console/actions-overview#available-flow-types).
- **Context:** Embedded goja JavaScript engine
- **Risks:** Shared memory space; errors could affect core functionality
- **Constraints:** Bound to ZITADEL process resources; limited timeout configuration
- **V1 Triggers:** Pre-event hooks (fire before operation) and Post-event hooks (fire after success)
### Actions V2: The Webhook Model
Custom logic lives in external HTTP endpoints called via webhooks.
- **Context:** Runs on your infrastructure (Cloudflare Workers, AWS Lambda, etc.)
- **Isolation:** Completely isolated memory; failures do not crash ZITADEL
- **Scaling:** Endpoints scale independently of ZITADEL
**V2 Execution Types:**
| Type | Description |
|------|-------------|
| **Request** | Triggers when a specific API request occurs |
| **Response** | Triggers when a specific API response occurs |
| **Function** | Triggers when specific functionality is used (useful for adding custom claims to tokens or executing external code during OIDC/SAML flows) |
| **Event** | Triggers reactively when ZITADEL events occur |
### Key Architectural Differences
| Aspect | Actions V1 | Actions V2 |
|--------|-----------|-----------|
| Execution Environment | Embedded in ZITADEL (sandboxed JavaScript) | External webhooks (Cloudflare Workers, AWS Lambda, etc.) |
| Code Location | Inline in ZITADEL Console | Hosted on your infrastructure |
| Language | JavaScript (limited runtime) | Any language |
| API Access | `ctx` and `api` objects provided by ZITADEL | ZITADEL REST/gRPC APIs via HTTP calls |
| Triggers | Flow + Trigger (e.g., "Pre Access Token Creation") | Specific API methods or webhook events |
| Dependencies | Limited (`zitadel/http`, `zitadel/log`) | Any framework |
| Security | Managed by ZITADEL | HMAC signature verification required (to validate incoming webhook requests from ZITADEL) |
| Scalability | Limited by ZITADEL instance | Independent scaling |
### Business Benefits
- **Technology Flexibility:** Use your preferred language instead of being locked into JavaScript
- **Operational Resilience:** Isolates failures to prevent cascading outages
- **Cost Optimization:** Leverage serverless "scale to zero" pricing for custom logic
- **Observability:** Integrate with tools such as Datadog and Sentry
## When to Use Each V2 Execution Type
We provide a collection of [ready-to-deploy Cloudflare Worker examples](https://github.com/zitadel/actions/tree/main/actions-v2-cloudflare-workers) that integrate with ZITADEL Actions V2. Each script demonstrates how to handle ZITADEL events, signatures, and responses directly from a serverless Cloudflare Worker environment. While the examples are designed for Cloudflare Workers, the code can be adapted to deploy on the platform of your choice (AWS Lambda, Azure Functions, etc.).
| If You Need To... | Use This V2 Type | When It Fires | Example |
|-------------------|------------------|---------------|---------|
| Add custom token claims | Function (`preaccesstoken`) | Before access token generated | [custom-claims](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/CUSTOM-CLAIMS.md) |
| Map roles to permissions | Function (`preaccesstoken`) | Before access token generated | [authorization](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/AUTHORIZATION.md) |
| Add "groups" claim from roles | Function (`preaccesstoken`/`preuserinfo`) | Before token/userinfo generated | [groups-claim](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/GROUPS-CLAIM.md) |
| Block login | Function (`preuserinfo`) + `interruptOnError: true` | Before userinfo generated | [block-login](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/BLOCK-LOGIN.md) |
| Track last login time | Function (`preuserinfo`) | Before userinfo generated | [set-user-metadata](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/SET-USER-METADATA.md) |
| Add custom SAML attributes | Function (`presamlresponse`) | Before SAML response sent | [saml-attributes](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/SAML-ATTRIBUTES.md) |
| Migrate users on first login | Request + Response (API methods) | Before/after user lookup & session | [jit-users-migration](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/JIT-USERS-MIGRATION.md) |
| Map IDP attributes | Response (`/RetrieveIdentityProviderIntent`) | After IDP authentication | [idp-mapping](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/IDP-MAPPING.md) |
| Auto-assign roles to new users | Event (`user.human.added`) | When user created | [set-role](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/SET-ROLE.md) |
| Forward events to monitoring | Event (any event) | When event occurs | [datadog-forwarder](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/DATADOG-FORWARDER.md) |
## Detailed Flow Explanations
### Flow 1: Internal Authentication
Maps user registration and login triggers to Request (Pre-operation) and Response (Post-operation) execution types.
**Example Scripts:**
- Block/validate requests: [block-login-flow.js](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/BLOCK-LOGIN.md)
- Add metadata post-creation: [metadata.js](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/SET-USER-METADATA.md)
### Flow 2: External Authentication
Handles IDP logins and JIT (Just-in-Time) provisioning via Response (Intent retrieval) and standard User Creation hooks.
**Example Scripts:**
- Map IDP attributes: [idp-mapping.js](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/IDP-MAPPING.md)
- JIT user migration: [jit-user-migration.js](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/JIT-USERS-MIGRATION.md)
### Flow 3: Complement Token
Moves from implicit hooks to specific V2 functions, primarily Function (`preaccesstoken`).
**Example Scripts:**
- Add custom claims: [custom-claims.js](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/CUSTOM-CLAIMS.md)
- Add permissions claim: [authorization.js](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/AUTHORIZATION.md)
### Flow 4: Customize SAML Response
Moves to the Function (`presamlresponse`).
**Example Scripts:**
- Add SAML attributes: [saml-attributes.js](https://github.com/zitadel/actions/blob/main/actions-v2-cloudflare-workers/SAML-ATTRIBUTES.md)
## Additional Resources
- [ZITADEL Actions V2 Documentation](/concepts/features/actions_v2)
- [Using Actions Guide](./usage)
- [Actions V2 Examples Repository](https://github.com/zitadel/actions/tree/main/actions-v2-cloudflare-workers)
@@ -0,0 +1,195 @@
---
title: Test Actions Event
---
This guide shows you how to leverage the ZITADEL actions feature to react to events in your ZITADEL instance.
You can use the actions feature to create a target that will be called when a specific event occurs.
This is useful for integrating with other systems or for triggering workflows based on events in ZITADEL.
## Prerequisites
Before you start, make sure you have everything set up correctly.
- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers)
- Your ZITADEL instance needs to have the actions feature enabled.
<Callout>
Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`.
In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL.
</Callout>
## Start example target
To test the actions feature, you need to create a target that will be called when an event occurs.
You will need to implement a listener that can receive HTTP requests and process the events.
For this example, we will use a simple Go HTTP server that will print the received events to standard output.
<Callout>
The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature).
</Callout>
```go
package main
import (
"fmt"
"io"
"net/http"
)
// webhook HandleFunc to read the request body and then print out the contents
func webhook(w http.ResponseWriter, req *http.Request) {
// read the body content
sentBody, err := io.ReadAll(req.Body)
if err != nil {
// if there was an error while reading the body return an error
http.Error(w, "error", http.StatusInternalServerError)
return
}
defer req.Body.Close()
// print out the read content
fmt.Println(string(sentBody))
}
func main() {
// handle the HTTP call under "/webhook"
http.HandleFunc("/webhook", webhook)
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
http.ListenAndServe(":8090", nil)
}
```
<Callout title="">
The example above runs only on your local machine (`localhost`).
To test it with Zitadel, you must make your listener reachable from the internet.
You can do this by using **Webhook.site** (see [Creating a Listener with Webhook.site](./webhook-site-setup)).
</Callout>
## Create target
As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the
target can be created as follows:
See [Create a target](/reference/api/action/zitadel.action.v2.ActionService.CreateTarget) for more detailed information.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/actions/targets' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"name": "local webhook",
"restWebhook": {
"interruptOnError": true
},
"endpoint": "http://localhost:8090/webhook",
"timeout": "10s"
}'
```
Save the returned ID to set in the execution.
## Set execution
To configure ZITADEL to call the target when an event occurs, you need to set an execution and define the event
condition.
See [Set an execution](/reference/api/action/zitadel.action.v2.ActionService.SetExecution) for more detailed information.
```shell
curl -L -X PUT 'https://${CUSTOM_DOMAIN}/v2/actions/executions' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"condition": {
"event": {
"event": "user.human.added"
}
},
"targets": [
"<TargetID returned>"
]
}'
```
## Example call
Now that you have set up the target and execution, you can test it by creating a user through the Management Console UI or
by calling the ZITADEL API to create a human user.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/users/new' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"organizationId": "336392597046099971",
"human":
{
"profile":
{
"givenName": "Minnie",
"familyName": "Mouse",
"nickName": "Mini",
"displayName": "Minnie Mouse",
"preferredLanguage": "en",
"gender": "GENDER_FEMALE"
},
"email":
{
"email": "[email protected]"
}
}
}'
```
Your server should now print out something like the following. Check out
the [Sent information Event](./usage#sent-information-event) payload description.
```json
{
"aggregateID": "336494809936035843",
"aggregateType": "user",
"resourceOwner": "336392597046099971",
"instanceID": "336392597046034435",
"version": "v2",
"sequence": 1,
"event_type": "user.human.added",
"created_at": "2025-09-05T08:55:36.156333Z",
"userID": "336392597046755331",
"event_payload":
{
"email": "[email protected]",
"gender": 1,
"lastName": "Mouse",
"nickName": "Mini",
"userName": "[email protected]",
"firstName": "Minnie",
"displayName": "Minnie Mouse",
"preferredLanguage": "en"
}
}
```
The event_payload is base64 encoded and has the following content:
```json
{
"email": "[email protected]",
"gender": 1,
"lastName": "Mouse",
"nickName": "Mini",
"userName": "[email protected]",
"firstName": "Minnie",
"displayName": "Minnie Mouse",
"preferredLanguage": "en"
}
```
## Conclusion
You have successfully set up a target and execution to react to events in your ZITADEL instance.
This feature can now be used to integrate with your existing systems to create custom workflows or automate tasks based on events in ZITADEL.
Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2).
@@ -0,0 +1,159 @@
---
title: Test Actions Function Manipulation
---
This guide shows you how to leverage the ZITADEL actions feature to enhance different functions in your ZITADEL instance.
You can use the actions feature to create a target that will be called when a specific functionality is used.
This is useful for integrating with other systems which need specific claims in tokens or for executing external code during OIDC or SAML flows.
## Prerequisites
Before you start, make sure you have everything set up correctly.
- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers)
- Your ZITADEL instance needs to have the actions feature enabled.
<Callout>
Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`.
In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL.
</Callout>
## Available functions
The available conditions can be found under [all available Functions](/reference/api/action/zitadel.action.v2.ActionService.ListExecutionFunctions).
## Start example target
To test the actions feature, you need to create a target that will be called when a function is used.
You will need to implement a listener that can receive HTTP requests and process the data.
For this example, we will use a simple Go HTTP server that will send back static data.
<Callout>
The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature).
</Callout>
```go
package main
import (
"encoding/json"
"net/http"
)
type Response struct {
SetUserMetadata []*Metadata `json:"set_user_metadata,omitempty"`
AppendClaims []*AppendClaim `json:"append_claims,omitempty"`
AppendLogClaims []string `json:"append_log_claims,omitempty"`
}
type Metadata struct {
Key string `json:"key"`
Value []byte `json:"value"`
}
type AppendClaim struct {
Key string `json:"key"`
Value any `json:"value"`
}
// call HandleFunc to respond with static data
func call(w http.ResponseWriter, req *http.Request) {
// create the response with the correct structure
resp := &Response{
SetUserMetadata: []*Metadata{
{Key: "key", Value: []byte("value")},
},
AppendClaims: []*AppendClaim{
{Key: "claim", Value: "value"},
},
AppendLogClaims: []string{"log1", "log2", "log3"},
}
data, err := json.Marshal(resp)
if err != nil {
// if there was an error while marshalling the json
http.Error(w, "error", http.StatusInternalServerError)
return
}
w.Write(data)
}
func main() {
// handle the HTTP call under "/call"
http.HandleFunc("/call", call)
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
http.ListenAndServe(":8090", nil)
}
```
<Callout title="">
The example above runs only on your local machine (`localhost`).
To test it with Zitadel, you must make your listener reachable from the internet.
You can do this by using **Webhook.site** (see [Creating a Listener with Webhook.site](./webhook-site-setup)).
</Callout>
## Create target
As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as call, the target can be created as follows:
See [Create a target](/reference/api/action/zitadel.action.v2.ActionService.CreateTarget) for more detailed information.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/actions/targets' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"name": "local call",
"restCall": {
"interruptOnError": true
},
"endpoint": "http://localhost:8090/call",
"timeout": "10s"
}'
```
Save the returned ID to set in the execution.
## Set execution
To configure ZITADEL to call the target when a function is executed, you need to set an execution and define the function
condition.
See [Set an execution](/reference/api/action/zitadel.action.v2.ActionService.SetExecution) for more detailed information.
```shell
curl -L -X PUT 'https://${CUSTOM_DOMAIN}/v2/actions/executions' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"condition": {
"function": {
"name": "preuserinfo"
}
},
"targets": [
"<TargetID returned>"
]
}'
```
## Example call
Now that you have set up the target and execution, you can test it by logging into Management Console UI or
by using any OIDC flow.
As a result 3 things happen:
- the user get the metadata with the key "key" and value "value" added
- the token has a claim "urn:zitadel:iam:claim" added with value "value"
- the token has the log claim "urn:zitadel:iam:action:preuserinfo:log" added with values "log1", "log2" and "log3".
For any further information related to [the OIDC Flow, refer to our documentation.](/guides/integrate/login/oidc/login-users)
## Conclusion
You have successfully set up a target and execution to react to functions in your ZITADEL instance.
This feature can now be used to integrate with your existing systems to create custom workflows or automate tasks based on functionality in ZITADEL.
Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2).
@@ -0,0 +1,175 @@
---
title: Test Actions Function
---
This guide shows you how to leverage the ZITADEL actions feature to enhance different functions in your ZITADEL instance.
You can use the actions feature to create a target that will be called when a specific functionality is used.
This is useful for integrating with other systems which need specific claims in tokens or for executing external code during OIDC or SAML flows.
## Prerequisites
Before you start, make sure you have everything set up correctly.
- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers)
- Your ZITADEL instance needs to have the actions feature enabled.
<Callout>
Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`.
In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL.
</Callout>
## Available functions
The available conditions can be found under [all available Functions](/reference/api/action/zitadel.action.v2.ActionService.ListExecutionFunctions).
## Start example target
To test the actions feature, you need to create a target that will be called when a function is used.
You will need to implement a listener that can receive HTTP requests and process the data.
For this example, we will use a simple Go HTTP server that will print the received data to standard output.
<Callout>
The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature).
</Callout>
```go
package main
import (
"fmt"
"io"
"net/http"
)
// webhook HandleFunc to read the request body and then print out the contents
func webhook(w http.ResponseWriter, req *http.Request) {
// read the body content
sentBody, err := io.ReadAll(req.Body)
if err != nil {
// if there was an error while reading the body return an error
http.Error(w, "error", http.StatusInternalServerError)
return
}
defer req.Body.Close()
// print out the read content
fmt.Println(string(sentBody))
}
func main() {
// handle the HTTP call under "/webhook"
http.HandleFunc("/webhook", webhook)
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
http.ListenAndServe(":8090", nil)
}
```
<Callout title="">
The example above runs only on your local machine (`localhost`).
To test it with Zitadel, you must make your listener reachable from the internet.
You can do this by using **Webhook.site** (see [Creating a Listener with Webhook.site](./webhook-site-setup)).
</Callout>
## Create target
As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows:
See [Create a target](/reference/api/action/zitadel.action.v2.ActionService.CreateTarget) for more detailed information.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/actions/targets' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"name": "local call",
"restWebhook": {
"interruptOnError": true
},
"endpoint": "http://localhost:8090/webhook",
"timeout": "10s"
}'
```
Save the returned ID to set in the execution.
## Set execution
To configure ZITADEL to call the target when a function is executed, you need to set an execution and define the function
condition.
See [Set an execution](/reference/api/action/zitadel.action.v2.ActionService.SetExecution) for more detailed information.
```shell
curl -L -X PUT 'https://${CUSTOM_DOMAIN}/v2/actions/executions' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"condition": {
"function": {
"name": "preuserinfo"
}
},
"targets": [
"<TargetID returned>"
]
}'
```
## Example call
Now that you have set up the target and execution, you can test it by logging into Management Console UI or
by using any OIDC flow.
Your server should now print out something like the following. Check out the [Sent information Function](./usage#sent-information-function) payload description.
```json
{
"function" : "function/preuserinfo",
"userinfo" : {
"sub" : "312909075212468632"
},
"user" : {
"id" : "312909075212468632",
"creation_date" : "2025-03-26T15:52:23.917636Z",
"change_date" : "2025-03-26T15:52:23.917636Z",
"resource_owner" : "312909075211944344",
"sequence" : 2,
"state" : 1,
"username" : "[email protected]",
"preferred_login_name" : "[email protected]",
"human" : {
"first_name" : "Example firstname",
"last_name" : "Example lastname",
"display_name" : "Example displayname",
"preferred_language" : "en",
"email" : "[email protected]",
"is_email_verified" : true,
"password_changed" : "0001-01-01T00:00:00Z",
"mfa_init_skipped" : "0001-01-01T00:00:00Z"
}
},
"user_metadata" : [ {
"creation_date" : "2025-03-27T09:10:25.879677Z",
"change_date" : "2025-03-27T09:10:25.879677Z",
"resource_owner" : "312909075211944344",
"sequence" : 18,
"key" : "key",
"value" : "dmFsdWU="
} ],
"org" : {
"id" : "312909075211944344",
"name" : "ZITADEL",
"primary_domain" : "example.com"
}
}
```
For any further information related to [the OIDC Flow, refer to our documentation.](/guides/integrate/login/oidc/login-users)
## Conclusion
You have successfully set up a target and execution to react to functions in your ZITADEL instance.
This feature can now be used to customize the functionality in ZITADEL, in particular the content of the OIDC tokens and SAML responses.
Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2).
@@ -0,0 +1,242 @@
---
title: Test Actions Request Manipulation
---
This guide shows you how to leverage the ZITADEL actions feature to manipulate API requests in your ZITADEL instance.
You can use the actions feature to create a target that will be called when a specific API request occurs.
This is useful for adding information to managed resources in ZITADEL.
## Prerequisites
Before you start, make sure you have everything set up correctly.
- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers)
- Your ZITADEL instance needs to have the actions feature enabled.
<Callout>
Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`.
In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL.
</Callout>
<Callout>
To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson),
as the request is a protocol buffer message, to avoid potential problems with the attribute names.
</Callout>
## Start example target
To test the actions feature, you need to create a target that will be called when an API endpoint is called.
You will need to implement a listener that can receive HTTP requests, process the request and returns the manipulated request.
For this example, we will use a simple Go HTTP server that will return the request with added metadata.
<Callout>
The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature).
</Callout>
```go
package main
import (
"encoding/json"
"io"
"net/http"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/user/v2"
"google.golang.org/protobuf/encoding/protojson"
)
type contextRequest struct {
Request *createUserRequestWrapper `json:"request"`
}
// createUserRequestWrapper necessary to marshal and unmarshal the JSON into the proto message correctly
type createUserRequestWrapper struct {
user.CreateUserRequest
}
func (r *createUserRequestWrapper) MarshalJSON() ([]byte, error) {
data, err := protojson.Marshal(r)
if err != nil {
return nil, err
}
return data, nil
}
func (r *createUserRequestWrapper) UnmarshalJSON(data []byte) error {
return protojson.Unmarshal(data, r)
}
// call HandleFunc to read the request body, manipulate the content and return the manipulated request
func call(w http.ResponseWriter, req *http.Request) {
// read the body content
sentBody, err := io.ReadAll(req.Body)
if err != nil {
// if there was an error while reading the body return an error
http.Error(w, "error", http.StatusInternalServerError)
return
}
defer req.Body.Close()
// read the request into the expected structure
request := new(contextRequest)
if err := json.Unmarshal(sentBody, request); err != nil {
http.Error(w, "error", http.StatusInternalServerError)
}
// build the response from the received request
response := request.Request
// manipulate the request to send back as response
metadata := response.GetHuman().GetMetadata()
if metadata == nil {
metadata = make([]*user.Metadata, 0)
}
metadata = append(metadata, &user.Metadata{Key: "organization", Value: []byte("dunder mifflin")})
response.GetHuman().Metadata = metadata
// marshal the request into json
data, err := json.Marshal(response)
if err != nil {
// if there was an error while marshalling the json
http.Error(w, "error", http.StatusInternalServerError)
return
}
// return the manipulated request
w.Write(data)
}
func main() {
// handle the HTTP call under "/call"
http.HandleFunc("/call", call)
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
http.ListenAndServe(":8090", nil)
}
```
<Callout title="">
The example above runs only on your local machine (`localhost`).
To test it with Zitadel, you must make your listener reachable from the internet.
You can do this by using **Webhook.site** (see [Creating a Listener with Webhook.site](./webhook-site-setup)).
</Callout>
## Create target
As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as call, the target can be created as follows:
See [Create a target](/reference/api/action/zitadel.action.v2.ActionService.CreateTarget) for more detailed information.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/actions/targets' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"name": "local call",
"restCall": {
"interruptOnError": true
},
"endpoint": "http://localhost:8090/call",
"timeout": "10s"
}'
```
Save the returned ID to set in the execution.
## Set execution
To call the target just created before, with the intention to manipulate the request used for user creation by the user V2 API, we define an execution with a method condition.
See [Set an execution](/reference/api/action/zitadel.action.v2.ActionService.SetExecution) for more detailed information.
```shell
curl -L -X PUT 'https://${CUSTOM_DOMAIN}/v2/actions/executions' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"condition": {
"request": {
"method": "/zitadel.user.v2.UserService/CreateUser"
}
},
"targets": [
"<TargetID returned>"
]
}'
```
## Example call
Now that you have set up the target and execution, you can test it by creating a user through the Management Console UI or
by calling the ZITADEL API to create a human user.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/users/new' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"organizationId": "344648897353810062",
"human":
{
"profile":
{
"givenName": "Minnie",
"familyName": "Mouse",
"nickName": "Mini",
"displayName": "Minnie Mouse",
"preferredLanguage": "en",
"gender": "GENDER_FEMALE"
},
"email":
{
"email": "[email protected]"
}
}
}'
```
Your server should now manipulate the request to something like the following. Check out
the [Sent information Request](./usage#sent-information-request) payload description.
```shell
curl -L -X PUT 'https://${CUSTOM_DOMAIN}/v2/users/new' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"organizationId": "344648897353810062",
"human":
{
"profile":
{
"givenName": "Minnie",
"familyName": "Mouse",
"nickName": "Mini",
"displayName": "Minnie Mouse",
"preferredLanguage": "en",
"gender": "GENDER_FEMALE"
},
"email":
{
"email": "[email protected]"
},
"metadata":
[
{
"key": "organization",
"value": "ZHVuZGVyIG1pZmZsaW4="
}
]
}
}'
```
## Conclusion
You have successfully set up a target and execution to manipulate API requests in your ZITADEL instance.
This feature can now be used to add or manipulate information to managed resources in ZITADEL.
Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2).
@@ -0,0 +1,240 @@
---
title: Verify Payload Integrity
---
import JSONCode from "./go/json.go";
import JWTCode from "./go/jwt.go";
import JWECode from "./go/jwe.go";
import CodeBlock from "@/components/docusaurus/code-block";
This guide shows you how to verify the integrity of the received data on your target.
There are three options available, which will be demonstrated in the following sections.
The examples are based on the request action, but the same principles apply to other action types as well.
## Prerequisites
Before you start, make sure you have everything set up correctly.
- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers)
<Callout type="info">
Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`.
In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL.
</Callout>
## Payload and Validation Types
ZITADEL supports the following three types of payload in actions:
- **JSON**: The payload is sent as JSON in the request body. Additionally, a signature header is sent to validate the payload.
This is the simplest form of payload and validation and doesn't require any additional calls to validate the payload. It's also the default type,
which will be used if no type is specified.
- **JWT**: The payload is sent as a JSON Web Token (JWT) in the request body. The JWT is signed with the signing key of the instance.
This allows you to validate the payload by verifying the signature of the JWT using the signing key managed though the webkeys endpoint of the ZITADEL instance,
which allows for easier key rotation and management. Additionally, it's the base for the JWE type, in case you need to encrypt the payload for certain use cases.
- **JWE**: The payload is sent as encrypted JSON Web Token (JWE) in the request body. The JWT is additionally encrypted with the public key provided by you.
This allows you to validate and decrypt the payload by verifying the signature of the JWT using the signing key managed though the webkeys endpoint of the ZITADEL instance,
and decrypting the payload using your private key. This is the most secure form of payload and validation, but requires additional setup to provide the public key to ZITADEL.
This type is recommended if the payload contains sensitive information that should not be exposed to any intermediaries.
## Create target
We'll start the endpoint on port '8090' and if we want to use it as webhook, the target can be created as follows:
See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information.
Specify the `payloadType` according to the implementation you want to test: `PAYLOAD_TYPE_JSON`, `PAYLOAD_TYPE_JWT`, or `PAYLOAD_TYPE_JWE`.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/actions/targets' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"name": "local webhook",
"restWebhook": {
"interruptOnError": true
},
"endpoint": "http://localhost:8090/webhook",
"timeout": "10s",
"payloadType": "PAYLOAD_TYPE_JSON"
}'
```
Example response after creating the target:
```json
{
"id": "344649040681500814",
"creationDate": "2025-10-31T15:00:36.432595dZ",
"signingKey": "somekey"
}
```
Save the returned ID to set in the execution. If you're intending to use the `PAYLOAD_TYPE_JSON`, additionally store the `signingKey` and use it in the example above.
### JWE Specific Setup
If you are using the `PAYLOAD_TYPE_JWE`, you need to provide a public key to ZITADEL so that it can encrypt the payload.
Create a public/private key pair. You can use the following command to generate an RSA key pair:
```shell
openssl genpkey -algorithm RSA -outform PEM -out private_key.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in private_key.pem -out public_key.pem
```
Then upload the public key to ZITADEL using the following command. Replace `<TargetID>` with the ID of the target you created earlier.
Use the base64 encoded content of the `public_key.pem` file as the value for `publicKey`.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/actions/targets/<TargetID>/publickeys' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"publicKey": "<base64 encoded contents of public_key.pem>"
}'
```
Be sure to also activate the public key for the target using the returned `<KeyID>` from the previous request:
```shell
curl -L -X PUT 'https://${CUSTOM_DOMAIN}/v2/actions/targets/<TargetID>/publickeys/<KeyID>/activate' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>'
```
## Set execution
To configure ZITADEL to call the target when an API endpoint is called, you need to set an execution and define the request
condition.
See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information.
```shell
curl -L -X PUT 'https://${CUSTOM_DOMAIN}/v2/actions/executions' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"condition": {
"request": {
"method": "/zitadel.user.v2.UserService/CreateUser"
}
},
"targets": [
"<TargetID returned>"
]
}'
```
## Start example target
To test the actions feature, you need to create a target that will be called when an API endpoint is called.
You will need to implement a listener that can receive HTTP requests.
For this example, we will use a simple Go HTTP server that will print the received request to standard output.
As mentioned before, this validation can and should be applied to any target implementation.
<Tabs>
<Tab value="JSON">
<CodeBlock language="go">{JSONCode}</CodeBlock>
</Tab>
<Tab value="JWT">
<CodeBlock language="go">{JWTCode}</CodeBlock>
</Tab>
<Tab value="JWE">
<CodeBlock language="go">{JWECode}</CodeBlock>
</Tab>
</Tabs>
<Callout type="info">
The example above runs only on your local machine (`localhost`).
To test it with Zitadel, you must make your listener reachable from the internet.
You can do this by using **Webhook.site** (see [Creating a Listener with Webhook.site](./webhook-site-setup)).
</Callout>
## Example call
Now that you have set up the target and execution, you can test it by creating a user through the Management Console UI or
by calling the ZITADEL API to create a human user.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/users/new' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"organizationId": "344648897353810062",
"human":
{
"profile":
{
"givenName": "Minnie",
"familyName": "Mouse",
"nickName": "Mini",
"displayName": "Minnie Mouse",
"preferredLanguage": "en",
"gender": "GENDER_FEMALE"
},
"email":
{
"email": "[email protected]"
}
}
}'
```
Your server should now print out something like the following. Check out
the [Sent information Request](./usage#sent-information-request) payload description.
```json
{
"fullMethod": "/zitadel.user.v2.UserService/CreateUser",
"instanceID": "344648897353744526",
"orgID": "344648897353810062",
"projectID": "344648897353875598",
"userID": "344648897354465422",
"request":
{
"organizationId": "344648897353810062",
"human":
{
"profile":
{
"givenName": "Minnie",
"familyName": "Mouse",
"nickName": "Mini",
"displayName": "Minnie Mouse",
"preferredLanguage": "en",
"gender": "GENDER_FEMALE"
},
"email":
{
"email": "[email protected]"
}
}
},
"headers":
{
"Content-Type":
[
"application/grpc"
],
"Host":
[
"localhost:8080"
],
"X-Forwarded-For":
[
"::1"
],
"X-Forwarded-Host":
[
"localhost:8080"
]
}
}
```
## Conclusion
You have successfully set up a target and verified the payload integrity for request actions using your preferred payload type.
You can now extend this setup to other action types and integrate it into your workflows as needed.
Selecting the appropriate payload type ensures that your data is transmitted securely and can be validated effectively on the receiving end.
Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2).
@@ -0,0 +1,209 @@
---
title: Test Actions Request
---
This guide shows you how to leverage the ZITADEL actions feature to react to API requests in your ZITADEL instance.
You can use the actions feature to create a target that will be called when a specific API request occurs.
This is useful for information provisioning in between systems or for triggering workflows based on API requests in ZITADEL.
## Prerequisites
Before you start, make sure you have everything set up correctly.
- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers)
- Your ZITADEL instance needs to have the actions feature enabled.
<Callout>
Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`.
In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL.
</Callout>
<Callout>
To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson),
as the request is a protocol buffer message, to avoid potential problems with the attribute names.
</Callout>
## Start example target
To test the actions feature, you need to create a target that will be called when an API endpoint is called.
You will need to implement a listener that can receive HTTP requests and process the request.
For this example, we will use a simple Go HTTP server that will print the received request to standard output.
<Callout>
The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature).
</Callout>
```go
package main
import (
"fmt"
"io"
"net/http"
)
// webhook HandleFunc to read the request body and then print out the contents
func webhook(w http.ResponseWriter, req *http.Request) {
// read the body content
sentBody, err := io.ReadAll(req.Body)
if err != nil {
// if there was an error while reading the body return an error
http.Error(w, "error", http.StatusInternalServerError)
return
}
defer req.Body.Close()
// print out the read content
fmt.Println(string(sentBody))
}
func main() {
// handle the HTTP call under "/webhook"
http.HandleFunc("/webhook", webhook)
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
http.ListenAndServe(":8090", nil)
}
```
<Callout title="">
The example above runs only on your local machine (`localhost`).
To test it with Zitadel, you must make your listener reachable from the internet.
You can do this by using **Webhook.site** (see [Creating a Listener with Webhook.site](./webhook-site-setup)).
</Callout>
## Create target
As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows:
See [Create a target](/reference/api/action/zitadel.action.v2.ActionService.CreateTarget) for more detailed information.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/actions/targets' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"name": "local webhook",
"restWebhook": {
"interruptOnError": true
},
"endpoint": "http://localhost:8090/webhook",
"timeout": "10s"
}'
```
Save the returned ID to set in the execution.
## Set execution
To configure ZITADEL to call the target when an API endpoint is called, you need to set an execution and define the request
condition.
See [Set an execution](/reference/api/action/zitadel.action.v2.ActionService.SetExecution) for more detailed information.
```shell
curl -L -X PUT 'https://${CUSTOM_DOMAIN}/v2/actions/executions' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"condition": {
"request": {
"method": "/zitadel.user.v2.UserService/CreateUser"
}
},
"targets": [
"<TargetID returned>"
]
}'
```
## Example call
Now that you have set up the target and execution, you can test it by creating a user through the Management Console UI or
by calling the ZITADEL API to create a human user.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/users/new' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"organizationId": "344648897353810062",
"human":
{
"profile":
{
"givenName": "Minnie",
"familyName": "Mouse",
"nickName": "Mini",
"displayName": "Minnie Mouse",
"preferredLanguage": "en",
"gender": "GENDER_FEMALE"
},
"email":
{
"email": "[email protected]"
}
}
}'
```
Your server should now print out something like the following. Check out
the [Sent information Request](./usage#sent-information-request) payload description.
The incoming request headers to the Execution are propagated via the request payload to the target.
```json
{
"fullMethod": "/zitadel.user.v2.UserService/CreateUser",
"instanceID": "344648897353744526",
"orgID": "344648897353810062",
"projectID": "344648897353875598",
"userID": "344648897354465422",
"request":
{
"organizationId": "344648897353810062",
"human":
{
"profile":
{
"givenName": "Minnie",
"familyName": "Mouse",
"nickName": "Mini",
"displayName": "Minnie Mouse",
"preferredLanguage": "en",
"gender": "GENDER_FEMALE"
},
"email":
{
"email": "[email protected]"
}
}
},
"headers":
{
"Content-Type":
[
"application/grpc"
],
"Host":
[
"localhost:8080"
],
"X-Forwarded-For":
[
"::1"
],
"X-Forwarded-Host":
[
"localhost:8080"
]
}
}
```
## Conclusion
You have successfully set up a target and execution to react to API requests in your ZITADEL instance.
This feature can now be used to provision information in between systems or for triggering workflows based on API requests in ZITADEL.
Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2).
@@ -0,0 +1,298 @@
---
title: Test Actions Response Manipulation
---
This guide shows you how to leverage the ZITADEL actions feature to manipulate API responses in your ZITADEL instance.
You can use the actions feature to create a target that will be called when a specific API response occurs.
This is useful for triggering workflows based on API responses in ZITADEL. You can even use this to provide data necessary data to the new login UI as shown in this example.
## Prerequisites
Before you start, make sure you have everything set up correctly.
- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers)
- Your ZITADEL instance needs to have the actions feature enabled.
<Callout>
Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`.
In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL.
</Callout>
<Callout>
To marshal and unmarshal the request and response please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson),
as the request and response are protocol buffer messages, to avoid potential problems with the attribute names.
</Callout>
## Start example target
To test the actions feature, you need to create a target that will be called when an API endpoint is called.
You will need to implement a listener that can receive HTTP requests, process the request and returns the manipulated request.
For this example, we will use a simple Go HTTP server that will return the request with added metadata.
<Callout>
The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature).
</Callout>
```go
package main
import (
"encoding/json"
"io"
"net/http"
"github.com/zitadel/zitadel-go/v3/pkg/client/zitadel/user/v2"
"google.golang.org/protobuf/encoding/protojson"
)
type contextResponse struct {
Request *retrieveIdentityProviderIntentRequestWrapper `json:"request"`
Response *retrieveIdentityProviderIntentResponseWrapper `json:"response"`
}
// RetrieveIdentityProviderIntentRequestWrapper necessary to marshal and unmarshal the JSON into the proto message correctly
type retrieveIdentityProviderIntentRequestWrapper struct {
user.RetrieveIdentityProviderIntentRequest
}
func (r *retrieveIdentityProviderIntentRequestWrapper) MarshalJSON() ([]byte, error) {
data, err := protojson.Marshal(r)
if err != nil {
return nil, err
}
return data, nil
}
func (r *retrieveIdentityProviderIntentRequestWrapper) UnmarshalJSON(data []byte) error {
return protojson.Unmarshal(data, r)
}
// RetrieveIdentityProviderIntentResponseWrapper necessary to marshal and unmarshal the JSON into the proto message correctly
type retrieveIdentityProviderIntentResponseWrapper struct {
user.RetrieveIdentityProviderIntentResponse
}
func (r *retrieveIdentityProviderIntentResponseWrapper) MarshalJSON() ([]byte, error) {
data, err := protojson.Marshal(r)
if err != nil {
return nil, err
}
return data, nil
}
func (r *retrieveIdentityProviderIntentResponseWrapper) UnmarshalJSON(data []byte) error {
return protojson.Unmarshal(data, r)
}
// call HandleFunc to read the response body, manipulate the content and return the response
func call(w http.ResponseWriter, req *http.Request) {
// read the body content
sentBody, err := io.ReadAll(req.Body)
if err != nil {
// if there was an error while reading the body return an error
http.Error(w, "error", http.StatusInternalServerError)
return
}
defer req.Body.Close()
// read the response into the expected structure
request := new(contextResponse)
if err := json.Unmarshal(sentBody, request); err != nil {
http.Error(w, "error", http.StatusInternalServerError)
}
// build the response from the received response
resp := request.Response
// manipulate the received response to send back as response
if resp != nil && resp.AddHumanUser != nil {
// manipulate the response
resp.AddHumanUser.Metadata = append(resp.AddHumanUser.Metadata, &user.SetMetadataEntry{Key: "organization", Value: []byte("company")})
}
// marshal the response into json
data, err := json.Marshal(resp)
if err != nil {
// if there was an error while marshalling the json
http.Error(w, "error", http.StatusInternalServerError)
return
}
// return the manipulated response
w.Write(data)
}
func main() {
// handle the HTTP call under "/call"
http.HandleFunc("/call", call)
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
http.ListenAndServe(":8090", nil)
}
```
<Callout title="">
The example above runs only on your local machine (`localhost`).
To test it with Zitadel, you must make your listener reachable from the internet.
You can do this by using **Webhook.site** (see [Creating a Listener with Webhook.site](./webhook-site-setup)).
</Callout>
## Create target
As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as call, the
target can be created as follows:
See [Create a target](/reference/api/action/zitadel.action.v2.ActionService.CreateTarget) for more detailed information.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/actions/targets' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"name": "local call",
"restCall": {
"interruptOnError": true
},
"endpoint": "http://localhost:8090/call",
"timeout": "10s"
}'
```
Save the returned ID to set in the execution.
## Set execution
To call the target just created before, with the intention to manipulate the retrieve of an intent by the user V2 API,
we define an execution with a response condition.
See [Set an execution](/reference/api/action/zitadel.action.v2.ActionService.SetExecution) for more detailed information.
```shell
curl -L -X PUT 'https://${CUSTOM_DOMAIN}/v2/actions/executions' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"condition": {
"response": {
"method": "/zitadel.user.v2.UserService/RetrieveIdentityProviderIntent"
}
},
"targets": [
"<TargetID returned>"
]
}'
```
## Example call
Now that you have set up the target and execution, you can test it by using a login-flow in the typescript login with an external IDP.
```json
{
"details": {
"sequence": "599",
"changeDate": "2023-06-15T06:44:26.039444Z",
"resourceOwner": "163840776835432705"
},
"idpInformation": {
"oauth": {
"accessToken": "ya29...",
"idToken": "ey..."
},
"idpId": "218528353504723201",
"userId": "218528353504723202",
"username": "test-user@localhost",
"rawInformation": {
"User": {
"email": "test-user@localhost",
"email_verified": true,
"family_name": "User",
"given_name": "Test",
"hd": "mouse.com",
"locale": "de",
"name": "Minnie Mouse",
"picture": "https://lh3.googleusercontent.com/a/AAcKTtf973Q7NH8KzKTMEZELPU9lx45WpQ9FRBuxFdPb=s96-c",
"sub": "111392805975715856637"
}
}
},
"addHumanUser": {
"idpLinks": [
{"idpId": "218528353504723201", "userId": "218528353504723202", "userName": "test-user@localhost"}
],
"username": "test-user@localhost",
"profile": {
"givenName": "Test",
"familyName": "User",
"displayName": "Test User",
"preferredLanguage": "de"
},
"email": {
"email": "[email protected]",
"isVerified": true
},
"metadata": []
}
}
```
Your server should now manipulate the response to something like the following. Check out
the [Sent information Response](./usage#sent-information-response) payload description.
```json
{
"details": {
"sequence": "599",
"changeDate": "2023-06-15T06:44:26.039444Z",
"resourceOwner": "163840776835432705"
},
"idpInformation": {
"oauth": {
"accessToken": "ya29...",
"idToken": "ey..."
},
"idpId": "218528353504723201",
"userId": "218528353504723202",
"username": "test-user@localhost",
"rawInformation": {
"User": {
"email": "test-user@localhost",
"email_verified": true,
"family_name": "User",
"given_name": "Test",
"hd": "mouse.com",
"locale": "de",
"name": "Minnie Mouse",
"picture": "https://lh3.googleusercontent.com/a/AAcKTtf973Q7NH8KzKTMEZELPU9lx45WpQ9FRBuxFdPb=s96-c",
"sub": "111392805975715856637"
}
}
},
"addHumanUser": {
"idpLinks": [
{"idpId": "218528353504723201", "userId": "218528353504723202", "userName": "test-user@localhost"}
],
"username": "test-user@localhost",
"profile": {
"givenName": "Test",
"familyName": "User",
"displayName": "Test User",
"preferredLanguage": "de"
},
"email": {
"email": "[email protected]",
"isVerified": true
},
"metadata": [
{"key": "organization", "value": "Y29tcGFueQ=="}
]
}
}
```
## Conclusion
You have successfully set up a target and execution to manipulate API responses in your ZITADEL instance.
This feature can now be used to add necessary information for clients including the new login UI.
Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2).
@@ -0,0 +1,214 @@
---
title: Test Actions Response
---
This guide shows you how to leverage the ZITADEL actions feature to react to API responses in your ZITADEL instance.
You can use the actions feature to create a target that will be called when a specific API response occurs.
This is useful for information provisioning in between systems or for triggering workflows based on API responses in ZITADEL.
## Prerequisites
Before you start, make sure you have everything set up correctly.
- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers)
- Your ZITADEL instance needs to have the actions feature enabled.
<Callout>
Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`.
In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL.
</Callout>
<Callout>
To marshal and unmarshal the request and response please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson),
as the request and response are protocol buffer messages, to avoid potential problems with the attribute names.
</Callout>
## Start example target
To test the actions feature, you need to create a target that will be called when an API endpoint is called.
You will need to implement a listener that can receive HTTP requests and process the request.
For this example, we will use a simple Go HTTP server that will print the received request to standard output.
<Callout>
The signature of the received request can be checked, [please refer to the example for more information on how to](/guides/integrate/actions/testing-request-signature).
</Callout>
```go
package main
import (
"fmt"
"io"
"net/http"
)
// webhook HandleFunc to read the request body and then print out the contents
func webhook(w http.ResponseWriter, req *http.Request) {
// read the body content
sentBody, err := io.ReadAll(req.Body)
if err != nil {
// if there was an error while reading the body return an error
http.Error(w, "error", http.StatusInternalServerError)
return
}
defer req.Body.Close()
// print out the read content
fmt.Println(string(sentBody))
}
func main() {
// handle the HTTP call under "/webhook"
http.HandleFunc("/webhook", webhook)
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
http.ListenAndServe(":8090", nil)
}
```
<Callout title="">
The example above runs only on your local machine (`localhost`).
To test it with Zitadel, you must make your listener reachable from the internet.
You can do this by using **Webhook.site** (see [Creating a Listener with Webhook.site](./webhook-site-setup)).
</Callout>
## Create target
As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows:
See [Create a target](/reference/api/action/zitadel.action.v2.ActionService.CreateTarget) for more detailed information.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/actions/targets' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"name": "local webhook",
"restWebhook": {
"interruptOnError": true
},
"endpoint": "http://localhost:8090/webhook",
"timeout": "10s"
}'
```
Save the returned ID to set in the execution.
## Set execution
To configure Zitadel to call the target when an API endpoint is called, you need to set an execution and define the response
condition.
See [Set an execution](/reference/api/action/zitadel.action.v2.ActionService.SetExecution) for more detailed information.
```shell
curl -L -X PUT 'https://${CUSTOM_DOMAIN}/v2/actions/executions' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"condition": {
"response": {
"method": "/zitadel.user.v2.UserService/CreateUser"
}
},
"targets": [
"<TargetID returned>"
]
}'
```
## Example call
Now that you have set up the target and execution, you can test it by creating a user through the Management Console UI or
by calling the ZITADEL API to create a human user.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/users/new' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"organizationId": "344648897353810062",
"human":
{
"profile":
{
"givenName": "Minnie",
"familyName": "Mouse",
"nickName": "Mini",
"displayName": "Minnie Mouse",
"preferredLanguage": "en",
"gender": "GENDER_FEMALE"
},
"email":
{
"email": "[email protected]"
}
}
}'
```
Your server should now print out something like the following. Check out
the [Sent information Response](./usage#sent-information-response) payload description.
The incoming request headers to the Execution are propagated via the request payload to the target.
```json
{
"fullMethod": "/zitadel.user.v2.UserService/CreateUser",
"instanceID": "344648897353744526",
"orgID": "344648897353810062",
"projectID": "344648897353875598",
"userID": "344648897354465422",
"request":
{
"organizationId": "344648897353810062",
"human":
{
"profile":
{
"givenName": "Minnie",
"familyName": "Mouse",
"nickName": "Mini",
"displayName": "Minnie Mouse",
"preferredLanguage": "en",
"gender": "GENDER_FEMALE"
},
"email":
{
"email": "[email protected]"
}
}
},
"response":
{
"id": "336494809936035843",
"creationDate": "2025-09-05T08:55:36.156333Z"
},
"headers":
{
"Content-Type":
[
"application/grpc"
],
"Host":
[
"localhost:8080"
],
"X-Forwarded-For":
[
"::1"
],
"X-Forwarded-Host":
[
"localhost:8080"
]
}
}
```
## Conclusion
You have successfully set up a target and execution to react to API responses in your ZITADEL instance.
This feature can now be used to provision information in between systems or for triggering workflows based on API responses in ZITADEL.
Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2).
@@ -0,0 +1,491 @@
---
title: Using Actions
---
The Action API provides a flexible mechanism for customizing and extending the functionality of ZITADEL. By allowing you to define targets and executions, you can implement custom workflows triggered on an API requests and responses, events or specific functions.
**How it works:**
- Create Target
- Set Execution with condition and target
- Custom Code will be triggered and executed
**Use Cases:**
- User Management: Automate provisioning user data to external systems when users are created, updated or deleted.
- Security: Implement IP blocking or rate limiting based on API usage patterns.
- Extend Workflows: Automatically setup resources in your application, when a new organization in ZITADEL is created.
- Token extension: Add custom claims to the tokens.
## Endpoints
ZITADEL sends an HTTP Post request to the endpoint set as Target, the received request than can be edited and send back or custom processes can be handled.
### Sent information Request
The information sent to the Endpoint is structured as JSON:
```json
{
"fullMethod": "full method of the GRPC call",
"instanceID": "instanceID of the called instance",
"orgID": "ID of the organization related to the calling context",
"projectID": "ID of the project related to the used application",
"userID": "ID of the calling user",
"request": {
"attribute": "Attribute value of full request of the call"
}
}
```
<Callout>
To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson),
as the request is a protocol buffer message, to avoid potential problems with the attribute names.
</Callout>
### Sent information Response
The information sent to the Endpoint is structured as JSON:
```json
{
"fullMethod": "full method of the GRPC call",
"instanceID": "instanceID of the called instance",
"orgID": "ID of the organization related to the calling context",
"projectID": "ID of the project related to the used application",
"userID": "ID of the calling user",
"request": {
"attribute": "Attribute value of full request of the call"
},
"response": {
"attribute": "Attribute value of full response of the call"
}
}
```
<Callout>
To marshal and unmarshal the request and response please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson),
as the request and response are protocol buffer messages, to avoid potential problems with the attribute names.
</Callout>
### Sent information Function
Information sent and expected back are specific to the function.
#### PreUserinfo
The information sent to the Endpoint is structured as JSON:
```json
{
"function": "Name of the function",
"userinfo": {
"given_name": "",
"family_name": "",
"middle_name": "",
"nickname": "",
"profile": "",
"picture": "",
...
"preferred_username": "",
"email": "",
"email_verified": true,
"phone_number": "",
"phone_number_verified": true
},
"user": {
"id": "",
"creation_date": "",
...
"human": {
"first_name": "",
"last_name": "",
...
"email": "",
"is_email_verified": true,
"phone": "",
"is_phone_verified": true
}
},
"user_metadata": [
{
"creation_date": "",
"change_date": "",
"resource_owner": "",
"sequence": "",
"key": "",
"value": ""
}
],
"org": {
"id": "ID of the organization the user belongs to",
"name": "Name of the organization the user belongs to",
"primary_domain": "Primary domain of the organization the user belongs to"
},
"user_grants": [
{
"id": "",
"projectGrantId": "The ID of the project grant",
"state": 1,
"creationDate": "",
"changeDate": "",
"sequence": 1,
"userId": "",
"roles": [
"role"
],
"userResourceOwner": "The ID of the organization the user belongs to",
"userGrantResourceOwner": "The ID of the organization the user got authorization granted",
"userGrantResourceOwnerName": "The name of the organization the user got authorization granted",
"projectId": "",
"projectName": ""
}
]
}
```
The expected structure of the JSON as response:
```json
{
"set_user_metadata": [
{
"key": "key of metadata to be set on the user",
"value": "base64 value of metadata to be set on the user"
}
],
"append_claims": [
{
"key": "key of claim to be set on the user",
"value": "value of claim to be set on the user"
}
],
"append_log_claims": [
"Log to be appended to the log claim on the token"
]
}
```
#### PreAccessToken
The information sent to the Endpoint is structured as JSON:
```json
{
"function": "Name of the function",
"userinfo": {
"given_name": "",
"family_name": "",
"middle_name": "",
"nickname": "",
"profile": "",
"picture": "",
...
"preferred_username": "",
"email": "",
"email_verified": true/false,
"phone_number": "",
"phone_number_verified": true/false
},
"user": {
"id": "",
"creation_date": "",
...
"human": {
"first_name": "",
"last_name": "",
...
"email": "",
"is_email_verified": true,
"phone": "",
"is_phone_verified": true
}
},
"user_metadata": [
{
"creation_date": "",
"change_date": "",
"resource_owner": "",
"sequence": "",
"key": "",
"value": ""
}
],
"org": {
"id": "ID of the organization the user belongs to",
"name": "Name of the organization the user belongs to",
"primary_domain": "Primary domain of the organization the user belongs to"
},
"user_grants": [
{
"id": "",
"projectGrantId": "The ID of the project grant",
"state": 1,
"creationDate": "",
"changeDate": "",
"sequence": 1,
"userId": "",
"roles": [
"role"
],
"userResourceOwner": "The ID of the organization the user belongs to",
"userGrantResourceOwner": "The ID of the organization the user got authorization granted",
"userGrantResourceOwnerName": "The name of the organization the user got authorization granted",
"projectId": "",
"projectName": ""
}
]
}
```
The expected structure of the JSON as response:
```json
{
"set_user_metadata": [
{
"key": "key of metadata to be set on the user",
"value": "base64 value of metadata to be set on the user"
}
],
"append_claims": [
{
"key": "key of claim to be set on the user",
"value": "value of claim to be set on the user"
}
],
"append_log_claims": [
"Log to be appended to the log claim on the token"
]
}
```
#### PreSAMLResponse
The information sent to the Endpoint is structured as JSON:
```json
{
"function": "Name of the function",
"userinfo": {
"given_name": "",
"family_name": "",
"middle_name": "",
"nickname": "",
"profile": "",
"picture": "",
...
"preferred_username": "",
"email": "",
"email_verified": true,
"phone_number": "",
"phone_number_verified": true
},
"user": {
"id": "",
"creation_date": "",
...
"human": {
"first_name": "",
"last_name": "",
...
"email": "",
"is_email_verified": true,
"phone": "",
"is_phone_verified": true
}
},
"user_grants": [
{
"id": "",
"projectGrantId": "The ID of the project grant",
"state": 1,
"creationDate": "",
"changeDate": "",
"sequence": 1,
"userId": "",
"roles": [
"role"
],
"userResourceOwner": "The ID of the organization the user belongs to",
"userGrantResourceOwner": "The ID of the organization the user got authorization granted",
"userGrantResourceOwnerName": "The name of the organization the user got authorization granted",
"projectId": "",
"projectName": ""
}
]
}
```
The expected structure of the JSON as response:
```json
{
"set_user_metadata": [
{
"key": "key of metadata to be set on the user",
"value": "base64 value of metadata to be set on the user"
}
],
"append_attribute": [
{
"name": "name of the attribute to be added to the response",
"name_format": "name format of the attribute to be added to the response",
"value": "value of the attribute to be added to the response"
}
]
}
```
### Sent information Event
The information sent to the Endpoint is structured as JSON:
```json
{
"aggregateID": "ID of the aggregate",
"aggregateType": "Type of the aggregate",
"resourceOwner": "Resourceowner the aggregate belongs to",
"instanceID": "ID of the instance the aggregate belongs to",
"version": "Version of the aggregate",
"sequence": "Sequence of the event",
"event_type": "Type of the event",
"created_at": "Time the event was created",
"userID": "ID of the creator of the event",
"event_payload": "Content of the event in JSON format"
}
```
## Target
The Target describes how ZITADEL interacts with the Endpoint.
There are different types of Targets:
- `Webhook`, the call handles the status code but response is irrelevant, can be InterruptOnError
- `Call`, the call handles the status code and response, can be InterruptOnError
- `Async`, the call handles neither status code nor response, but can be called in parallel with other Targets
`InterruptOnError` means that the Execution gets interrupted if any of the calls return with a status code >= 400, and the next Target will not be called anymore.
The API documentation to create a target can be found [here](/reference/api/action/zitadel.action.v2.ActionService.CreateTarget)
### Content Signing
To ensure the integrity of request content, each call includes a 'ZITADEL-Signature' in the headers. This header contains an HMAC value computed from the request content and a timestamp, which can be used to time out requests. The logic for this process is provided in 'pkg/actions/signing.go'. The goal is to verify that the HMAC value in the header matches the HMAC value computed by the Target, ensuring that the sent and received requests are identical.
Each Target resource now contains also a Signing Key, which gets generated and returned when a Target is [created](/reference/api/action/zitadel.action.v2.ActionService.CreateTarget),
and can also be newly generated when a Target is [patched](/reference/api/action/zitadel.action.v2.ActionService.UpdateTarget).
For an example on how to check the signature, [refer to the example](/guides/integrate/actions/testing-request-signature).
## Execution
ZITADEL decides on specific conditions if one or more Targets have to be called.
The Execution resource contains 2 parts, the condition and the called targets.
The condition can be defined for 4 types of processes:
- `Requests`, before a request is processed by ZITADEL
- `Responses`, before a response is sent back to the application
- `Functions`, handling specific functionality in the logic of ZITADEL
- `Events`, after a specific event happened and was stored in ZITADEL
The API documentation to set an Execution can be found [here](/reference/api/action/zitadel.action.v2.ActionService.SetExecution)
### Condition Best Match
As the conditions can be defined on different levels, ZITADEL tries to find out which Execution is the best match.
This means that for example if you have an Execution defined on `all requests`, on the service `zitadel.user.v2.UserService` and on `/zitadel.user.v2.UserService/AddHumanUser`,
ZITADEL would with a call on the `/zitadel.user.v2.UserService/AddHumanUser` use the Executions with the following priority:
1. `/zitadel.user.v2.UserService/AddHumanUser`
2. `zitadel.user.v2.UserService`
3. `all`
If you then have a call on `/zitadel.user.v2.UserService/UpdateHumanUser` the following priority would be found:
1. `zitadel.user.v2.UserService`
2. `all`
And if you use a different service, for example `zitadel.session.v2.SessionService`, then the `all` Execution would still be used.
### Targets
An execution can contain only a list of Targets, and Targets are comma separated string values.
Here's an example of a Target defined on a service (e.g. `zitadel.user.v2.UserService`)
```json
{
"condition": {
"request": {
"service": "zitadel.user.v2.UserService"
}
},
"targets": [
"<TargetID1>"
]
}
```
Here's an example of a Target defined on a method (e.g. `/zitadel.user.v2.UserService/AddHumanUser`)
```json
{
"condition": {
"request": {
"method": "/zitadel.user.v2.UserService/AddHumanUser"
}
},
"targets": [
"<TargetID2>",
"<TargetID1>"
]
}
```
The called Targets on `/zitadel.user.v2.UserService/AddHumanUser` would be, in order:
1. `<TargetID2>`
2. `<TargetID1>`
### Condition for Requests and Responses
For Request and Response there are 3 levels the condition can be defined:
- `Method`, handling a request or response of a specific GRPC full method, which includes the service name and method of the ZITADEL API
- `Service`, handling any request or response under a service of the ZITADEL API
- `All`, handling any request or response under the ZITADEL API
The available conditions can be found under:
- [All available Methods](/reference/api/action/zitadel.action.v2.ActionService.ListExecutionMethods), for example `/zitadel.user.v2.UserService/AddHumanUser`
- [All available Services](/reference/api/action/zitadel.action.v2.ActionService.ListExecutionServices), for example `zitadel.user.v2.UserService`
### Condition for Functions
The available conditions can be found under [all available Functions](/reference/api/action/zitadel.action.v2.ActionService.ListExecutionFunctions).
### Condition for Events
For event there are 3 levels the condition can be defined:
- Event, handling a specific event
- Group, handling a specific group of events
- All, handling any event in ZITADEL
The concept of events can be found under [Events](/concepts/architecture/software#events)
### Error forwarding
If you want to forward a specific error from the Target through ZITADEL, you can provide a response from the Target with status code 200 and a JSON in the following format:
```json
{
"forwardedStatusCode": 403,
"forwardedErrorMessage": "Call is forbidden through the IP AllowList definition"
}
```
Only values from 400 to 499 will be forwarded through ZITADEL, other StatusCodes will end in a PreconditionFailed error.
If the Target returns any other status code than >= 200 and < 299, the execution is looked at as failed, and a PreconditionFailed error is logged.
<Callout>
To interrupt the execution while forwarding this error, "interruptOnError" must be **true** for the Target
</Callout>
@@ -0,0 +1,172 @@
---
title: Testing Actions V2 with Webhook.site
sidebar_label: Testing Actions V2 with Webhook.site
---
## 1. Introduction
This guide explains how to test **ZITADEL Actions V2** locally using [Webhook.site](https://webhook.site).
We will use Webhook.site’s **XHR Redirect** feature to forward requests from ZITADEL to your local machine.
**What you will learn in this guide:**
- How to add the required CORS headers to your local listener
- How to configure Webhook.site with XHR Redirect
- How to create a target in ZITADEL and link it to your Action
- How to verify forwarded requests in your terminal
---
## 2. Prerequisites
### 2.1. Local Listener
You should already have a local HTTP server (for example, the Go example listener from [Testing a Request](./testing-request)) running at `http://localhost:8090/webhook`.
### 2.2. CORS Headers
Because Webhook.site’s XHR redirect runs in your browser, your listener must allow CORS.
Add the following headers in your handler:
```go
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if req.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
```
---
## 3. Configure Webhook.site XHR Redirect
1. Open your unique Webhook.site URL.
2. Click **Configure XHR Redirect**.
3. Enable **XHR Redirect**.
4. Enter your local endpoint in **Target**, e.g.:
```
http://localhost:8090/webhook
```
5. Leave other fields empty unless you need custom headers.
6. Keep the Webhook.site browser tab open while testing.
<img src="/docs/img/actions-v2/webhook-xhr.png" alt="Screenshot: Webhook.site XHR Redirect configuration interface"/>
---
## 4. Run Your Listener
Start your local listener:
```sh
go run actionsRequest.go
```
You should see output in your console whenever the listener is called.
<img src="/docs/img/actions-v2/console.png" alt="Screenshot: Console output showing listener receiving webhook requests"/>
---
## 5. Create Target in ZITADEL
As shown in the example above, the target is created with HTTP and port '8090'. If you want to use it as a webhook, the target can be created as follows:
See [Create a target](/reference/api/action/zitadel.action.v2.ActionService.CreateTarget) for more detailed information. Notice that the `endpoint` is your Webhook.site URL.
```shell
curl -L -X POST 'https://${CUSTOM_DOMAIN}/v2/actions/targets' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"name": "local webhook",
"restWebhook": {
"interruptOnError": true
},
"endpoint": "https://webhook.site/29fa2769-e2f6-44ff-8eb0-2434b83507d8/webhook",
"timeout": "10s"
}'
```
<img src="/docs/img/actions-v2/webhook-url.png" alt="Screenshot: Webhook.site target creation interface"/>
---
Save the returned ID to use in the execution step. A sample response looks like this:
```json
{
"id": "337246363446151234",
"creationDate": "2025-09-10T13:21:36.959699Z",
"signingKey": "OpUHaCtEqh8swdJ5xUYbQ2bhej1abcXYZ"
}
```
## 6. Set execution
To configure ZITADEL to call the target when an API endpoint is called, set an execution and define the request condition.
See [Set an execution](/reference/api/action/zitadel.action.v2.ActionService.SetExecution) for more detailed information.
Here, `<TargetID returned>` is the `id` from the previous step.
```shell
curl -L -X PUT 'https://${CUSTOM_DOMAIN}/v2/actions/executions' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"condition": {
"request": {
"method": "/zitadel.user.v2.UserService/AddHumanUser"
}
},
"targets": [
"<TargetID returned>"
]
}'
```
---
## 7. Example Call
Now that you have set up the target and execution, you can test it by creating a user through the Management Console UI or by calling the ZITADEL API to create a human user.
Here, we are creating a user via the API:
```shell
curl -L -X PUT 'https://${CUSTOM_DOMAIN}/v2/users/human' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"profile": {
"givenName": "Example_given",
"familyName": "Example_family"
},
"email": {
"email": "[email protected]"
}
}'
```
Both your Webhook.site and your local listener should now print out something like the following. See the [Sent information Request](./usage#sent-information-request) payload description for details.
Your local listener should look like this:
<img src="/docs/img/actions-v2/local-listener.png" alt="Screenshot: Local listener output"/>
Your Webhook.site should look like this:
<img src="/docs/img/actions-v2/webhook-listener.png" alt="Screenshot: Webhook.site request log"/>
---
## 8. Done
You now have a fully working setup for testing ZITADEL Actions V2 with Webhook.site. This allows you to forward requests securely from ZITADEL to your local environment without needing a public IP address or domain.