curl --request PUT \
--url https://brew.new/api/v1/automations/triggers/{triggerEventId}/contract \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"fields": [
{
"key": "email",
"type": "string",
"required": true
},
{
"key": "plan",
"type": "enum",
"required": false,
"enumValues": [
"free",
"pro"
],
"fallbackValue": "free",
"description": "Billing plan at signup time."
},
{
"key": "order",
"type": "object",
"required": false,
"children": [
{
"key": "total",
"type": "float",
"required": true
}
]
}
]
}
'import requests
url = "https://brew.new/api/v1/automations/triggers/{triggerEventId}/contract"
payload = { "fields": [
{
"key": "email",
"type": "string",
"required": True
},
{
"key": "plan",
"type": "enum",
"required": False,
"enumValues": ["free", "pro"],
"fallbackValue": "free",
"description": "Billing plan at signup time."
},
{
"key": "order",
"type": "object",
"required": False,
"children": [
{
"key": "total",
"type": "float",
"required": True
}
]
}
] }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
fields: [
{key: 'email', type: 'string', required: true},
{
key: 'plan',
type: 'enum',
required: false,
enumValues: ['free', 'pro'],
fallbackValue: 'free',
description: 'Billing plan at signup time.'
},
{
key: 'order',
type: 'object',
required: false,
children: [{key: 'total', type: 'float', required: true}]
}
]
})
};
fetch('https://brew.new/api/v1/automations/triggers/{triggerEventId}/contract', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://brew.new/api/v1/automations/triggers/{triggerEventId}/contract",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'fields' => [
[
'key' => 'email',
'type' => 'string',
'required' => true
],
[
'key' => 'plan',
'type' => 'enum',
'required' => false,
'enumValues' => [
'free',
'pro'
],
'fallbackValue' => 'free',
'description' => 'Billing plan at signup time.'
],
[
'key' => 'order',
'type' => 'object',
'required' => false,
'children' => [
[
'key' => 'total',
'type' => 'float',
'required' => true
]
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://brew.new/api/v1/automations/triggers/{triggerEventId}/contract"
payload := strings.NewReader("{\n \"fields\": [\n {\n \"key\": \"email\",\n \"type\": \"string\",\n \"required\": true\n },\n {\n \"key\": \"plan\",\n \"type\": \"enum\",\n \"required\": false,\n \"enumValues\": [\n \"free\",\n \"pro\"\n ],\n \"fallbackValue\": \"free\",\n \"description\": \"Billing plan at signup time.\"\n },\n {\n \"key\": \"order\",\n \"type\": \"object\",\n \"required\": false,\n \"children\": [\n {\n \"key\": \"total\",\n \"type\": \"float\",\n \"required\": true\n }\n ]\n }\n ]\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://brew.new/api/v1/automations/triggers/{triggerEventId}/contract")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"fields\": [\n {\n \"key\": \"email\",\n \"type\": \"string\",\n \"required\": true\n },\n {\n \"key\": \"plan\",\n \"type\": \"enum\",\n \"required\": false,\n \"enumValues\": [\n \"free\",\n \"pro\"\n ],\n \"fallbackValue\": \"free\",\n \"description\": \"Billing plan at signup time.\"\n },\n {\n \"key\": \"order\",\n \"type\": \"object\",\n \"required\": false,\n \"children\": [\n {\n \"key\": \"total\",\n \"type\": \"float\",\n \"required\": true\n }\n ]\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://brew.new/api/v1/automations/triggers/{triggerEventId}/contract")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"fields\": [\n {\n \"key\": \"email\",\n \"type\": \"string\",\n \"required\": true\n },\n {\n \"key\": \"plan\",\n \"type\": \"enum\",\n \"required\": false,\n \"enumValues\": [\n \"free\",\n \"pro\"\n ],\n \"fallbackValue\": \"free\",\n \"description\": \"Billing plan at signup time.\"\n },\n {\n \"key\": \"order\",\n \"type\": \"object\",\n \"required\": false,\n \"children\": [\n {\n \"key\": \"total\",\n \"type\": \"float\",\n \"required\": true\n }\n ]\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"subjectKind": "trigger",
"subjectId": "user_signed_up",
"source": "stored",
"format": "json",
"typeName": "UserSignedUpPayload",
"contractHash": "22545d11e1d174ba0717ed37c9c4b460c96bed51ae31ea0af18266ebba30f76a",
"version": 3,
"enforcement": "prune",
"fields": [
{
"key": "email",
"type": "string",
"required": true
},
{
"key": "plan",
"type": "enum",
"required": false,
"enumValues": [
"free",
"pro"
],
"fallbackValue": "free"
},
{
"key": "order",
"type": "object",
"required": false,
"children": [
{
"key": "total",
"type": "float",
"required": true
}
]
}
],
"warnings": [
{
"code": "ENFORCEMENT_LOOSENED_WHILE_PUBLISHED",
"message": "Enforcement loosened from strict to prune while 1 published automation(s) consume this trigger. Loosening is allowed as the operator escape hatch, but tightening back to strict will be refused with 409 CONTRACT_LOCKED_BY_PUBLISHED_AUTOMATIONS until they are unpublished or detached."
}
]
}Declare a trigger payload contract
Declares the stored payload contract for the trigger, changes how it is enforced, or both, in one call. fields and enforcement are both optional, so arming enforcement is { "enforcement": "strict" }. While a PUBLISHED automation consumes the trigger the change must stay backward compatible (the ratchet below); loosening is always allowed.
Use when you want generated types, a dry-run validator (validateTriggerPayload) and drift detection for the JSON your service fires, or when you flip enforcement. Draft the tree from a real payload with inferPayloadContract. Trigger contracts MUST keep a top-level { key: "email", type: "string", required: true } field so automations can resolve a recipient.
Input fields is a tree: scalars (string, int, float, boolean, date, enum), object nodes with children, array nodes with children (object elements), itemType (scalar elements) or neither. The tree is validated BEFORE any write: unknown-typed nodes are refused, keys are unique per level, an object declares at least one child. enforcement: off (advisory, the default for a new contract), prune (drop undeclared keys before the run), strict (reject a payload carrying them). A fallbackValue satisfies required in EVERY mode: a producer that omits the key never fails a fire, every recipient gets the fallback. The version bumps only when the behavioral surface changes (keys, types, required, fallbacks; not descriptions or examples). Declaring a contract never changes fire behavior by itself. Loosening is the escape hatch when strict rejects a drifted producer in production.
Returns 200 with the same body a follow-up getTriggerContract returns plus warnings[] (always present, often empty): ENFORCEMENT_LOOSENED_WHILE_PUBLISHED when enforcement went down while published automations consume the trigger (tightening back is refused until they are unpublished or detached), and one REQUIRED_FIELD_SATISFIED_BY_FALLBACK per required field that declares a fallback (field is its dotted path). Ratchet while a published automation consumes the trigger: tighten off→prune, off→strict, prune→strict = 409; loosen = 200 + warning; remove, retype or enum-narrow a field, or add a required field WITHOUT a fallback = 409; add an optional field, widen a type, add an enum value, or add a required field WITH a fallback = 200 (+ warning per fallback). With no published consumer every change is 200.
Errors 409 CONTRACT_LOCKED_BY_PUBLISHED_AUTOMATIONS for a change the ratchet refuses (message names the incompatibility; unpublish or detach the consumers, or make a compatible change); 404 TRIGGER_EVENT_NOT_FOUND; 400 INVALID_REQUEST (param: "fields") when the tree fails structural validation, or when a body with no fields targets a trigger that has no stored contract yet.
See also getTriggerContract, validateTriggerPayload, inferPayloadContract, updateAutomation (publishing a consumer starts the ratchet).
curl --request PUT \
--url https://brew.new/api/v1/automations/triggers/{triggerEventId}/contract \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"fields": [
{
"key": "email",
"type": "string",
"required": true
},
{
"key": "plan",
"type": "enum",
"required": false,
"enumValues": [
"free",
"pro"
],
"fallbackValue": "free",
"description": "Billing plan at signup time."
},
{
"key": "order",
"type": "object",
"required": false,
"children": [
{
"key": "total",
"type": "float",
"required": true
}
]
}
]
}
'import requests
url = "https://brew.new/api/v1/automations/triggers/{triggerEventId}/contract"
payload = { "fields": [
{
"key": "email",
"type": "string",
"required": True
},
{
"key": "plan",
"type": "enum",
"required": False,
"enumValues": ["free", "pro"],
"fallbackValue": "free",
"description": "Billing plan at signup time."
},
{
"key": "order",
"type": "object",
"required": False,
"children": [
{
"key": "total",
"type": "float",
"required": True
}
]
}
] }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
fields: [
{key: 'email', type: 'string', required: true},
{
key: 'plan',
type: 'enum',
required: false,
enumValues: ['free', 'pro'],
fallbackValue: 'free',
description: 'Billing plan at signup time.'
},
{
key: 'order',
type: 'object',
required: false,
children: [{key: 'total', type: 'float', required: true}]
}
]
})
};
fetch('https://brew.new/api/v1/automations/triggers/{triggerEventId}/contract', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://brew.new/api/v1/automations/triggers/{triggerEventId}/contract",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'fields' => [
[
'key' => 'email',
'type' => 'string',
'required' => true
],
[
'key' => 'plan',
'type' => 'enum',
'required' => false,
'enumValues' => [
'free',
'pro'
],
'fallbackValue' => 'free',
'description' => 'Billing plan at signup time.'
],
[
'key' => 'order',
'type' => 'object',
'required' => false,
'children' => [
[
'key' => 'total',
'type' => 'float',
'required' => true
]
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://brew.new/api/v1/automations/triggers/{triggerEventId}/contract"
payload := strings.NewReader("{\n \"fields\": [\n {\n \"key\": \"email\",\n \"type\": \"string\",\n \"required\": true\n },\n {\n \"key\": \"plan\",\n \"type\": \"enum\",\n \"required\": false,\n \"enumValues\": [\n \"free\",\n \"pro\"\n ],\n \"fallbackValue\": \"free\",\n \"description\": \"Billing plan at signup time.\"\n },\n {\n \"key\": \"order\",\n \"type\": \"object\",\n \"required\": false,\n \"children\": [\n {\n \"key\": \"total\",\n \"type\": \"float\",\n \"required\": true\n }\n ]\n }\n ]\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://brew.new/api/v1/automations/triggers/{triggerEventId}/contract")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"fields\": [\n {\n \"key\": \"email\",\n \"type\": \"string\",\n \"required\": true\n },\n {\n \"key\": \"plan\",\n \"type\": \"enum\",\n \"required\": false,\n \"enumValues\": [\n \"free\",\n \"pro\"\n ],\n \"fallbackValue\": \"free\",\n \"description\": \"Billing plan at signup time.\"\n },\n {\n \"key\": \"order\",\n \"type\": \"object\",\n \"required\": false,\n \"children\": [\n {\n \"key\": \"total\",\n \"type\": \"float\",\n \"required\": true\n }\n ]\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://brew.new/api/v1/automations/triggers/{triggerEventId}/contract")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"fields\": [\n {\n \"key\": \"email\",\n \"type\": \"string\",\n \"required\": true\n },\n {\n \"key\": \"plan\",\n \"type\": \"enum\",\n \"required\": false,\n \"enumValues\": [\n \"free\",\n \"pro\"\n ],\n \"fallbackValue\": \"free\",\n \"description\": \"Billing plan at signup time.\"\n },\n {\n \"key\": \"order\",\n \"type\": \"object\",\n \"required\": false,\n \"children\": [\n {\n \"key\": \"total\",\n \"type\": \"float\",\n \"required\": true\n }\n ]\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"subjectKind": "trigger",
"subjectId": "user_signed_up",
"source": "stored",
"format": "json",
"typeName": "UserSignedUpPayload",
"contractHash": "22545d11e1d174ba0717ed37c9c4b460c96bed51ae31ea0af18266ebba30f76a",
"version": 3,
"enforcement": "prune",
"fields": [
{
"key": "email",
"type": "string",
"required": true
},
{
"key": "plan",
"type": "enum",
"required": false,
"enumValues": [
"free",
"pro"
],
"fallbackValue": "free"
},
{
"key": "order",
"type": "object",
"required": false,
"children": [
{
"key": "total",
"type": "float",
"required": true
}
]
}
],
"warnings": [
{
"code": "ENFORCEMENT_LOOSENED_WHILE_PUBLISHED",
"message": "Enforcement loosened from strict to prune while 1 published automation(s) consume this trigger. Loosening is allowed as the operator escape hatch, but tightening back to strict will be refused with 409 CONTRACT_LOCKED_BY_PUBLISHED_AUTOMATIONS until they are unpublished or detached."
}
]
}Authorizations
Send your Brew API key as Authorization: Bearer brew_xxx.
Headers
The brand this request acts on. REQUIRED for organization-scoped credentials (otherwise 400 BRAND_ID_REQUIRED — there is no default brand); list ids with GET /v1/brands. Brand-scoped credentials may omit it, and sending a different brand returns 403 BRAND_SCOPE_MISMATCH. A brand outside your organization returns 404 BRAND_NOT_FOUND.
1 - 64Path Parameters
Trigger id returned by POST /v1/automations/triggers (integration triggers use composite ids — URL-encode the colons).
1 - 256"user_signed_up"
Body
Response
Stored. The contract as a follow-up GET would return it, plus warnings[] about the write.
trigger 1stored, derived_from_schema, derived_from_template 1json, ts, zod, jsonschema, skill Show child attributes
Show child attributes
64x >= 1off, prune, strict fresh, stale Show child attributes
Show child attributes
Was this page helpful?