Record a stewardship-plan review
Uses and increments the plan version. Records an append-only review; strategy reviews also refresh strategy-reviewed timestamps. Requires stewardship_major_donor_plans_v1. Relationship-group subjects also require stewardship_relationship_groups_v1. Subject access respects the viewer’s donor visibility. Management requires donors_manage/group_all, or donors_view with assignment as this subject’s fundraiser; strategy fields follow the same authority. Requires permission: donors_view; plus one of donors_view, donors_manage.
curl --request POST \
--url https://api.gratona.com/v1/admin/fundraising/stewardship-plans/{plan_id}/reviews \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"version": 1,
"review_type": "strategy",
"summary": "Summary",
"outcome": "Outcome"
}
'import requests
url = "https://api.gratona.com/v1/admin/fundraising/stewardship-plans/{plan_id}/reviews"
payload = {
"version": 1,
"review_type": "strategy",
"summary": "Summary",
"outcome": "Outcome"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({version: 1, review_type: 'strategy', summary: 'Summary', outcome: 'Outcome'})
};
fetch('https://api.gratona.com/v1/admin/fundraising/stewardship-plans/{plan_id}/reviews', 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://api.gratona.com/v1/admin/fundraising/stewardship-plans/{plan_id}/reviews",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'version' => 1,
'review_type' => 'strategy',
'summary' => 'Summary',
'outcome' => 'Outcome'
]),
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://api.gratona.com/v1/admin/fundraising/stewardship-plans/{plan_id}/reviews"
payload := strings.NewReader("{\n \"version\": 1,\n \"review_type\": \"strategy\",\n \"summary\": \"Summary\",\n \"outcome\": \"Outcome\"\n}")
req, _ := http.NewRequest("POST", 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.post("https://api.gratona.com/v1/admin/fundraising/stewardship-plans/{plan_id}/reviews")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"version\": 1,\n \"review_type\": \"strategy\",\n \"summary\": \"Summary\",\n \"outcome\": \"Outcome\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.gratona.com/v1/admin/fundraising/stewardship-plans/{plan_id}/reviews")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"version\": 1,\n \"review_type\": \"strategy\",\n \"summary\": \"Summary\",\n \"outcome\": \"Outcome\"\n}"
response = http.request(request)
puts response.read_body{
"status": "success",
"data": {
"id": 42,
"reviewed_at": "2026-10-20T12:00:00Z",
"warnings": [
{
"code": "work_projection_delayed",
"message": "Example"
}
]
}
}{
"message": "Unauthorized",
"status": "error"
}{
"message": "Not Found",
"status": "error"
}{
"status": "error",
"code": "PLAN_VERSION_CONFLICT",
"message": "Example",
"data": {
"latest": {
"id": 42,
"version": 2,
"subject": {
"type": "donor",
"id": 42
},
"stage": "qualifying",
"status": "draft",
"tier": "A",
"stage_entered_at": "2026-10-20T12:00:00Z",
"action_donor_id": 42,
"period": {
"start": "2026-10-20",
"end": "2026-10-20"
},
"currency": "USD",
"goal_amount": 1000,
"attributed_actual": 1000,
"direct_actual": 1000,
"soft_credit_actual": 1000,
"variance_amount": 1000,
"gift_count": 2,
"giving_status": "pending",
"giving_last_successful_at": "2026-10-20T12:00:00Z",
"currency_breakdown": [
{
"currency": "Example",
"attributed_actual": 1000,
"direct_actual": 1000,
"soft_credit_actual": 1000,
"gift_count": 2
}
],
"next_move": {
"id": 42,
"title": "Example",
"planned_at": "2026-10-20T12:00:00Z"
},
"open_move_count": 2,
"overdue_move_count": 2,
"health": {
"status": "Example",
"reasons": [
"overdue_move"
]
},
"metric_basis": "relationship_attribution"
},
"conflicting_plan": {}
}
}{
"errors": {},
"status": "error",
"message": "The given data was invalid."
}Authorizations
Passport bearer token authentication for legacy admin sessions.
Path Parameters
Plan id in the current organization.
Body
Validated request fields for this operation.
Current plan version for optimistic concurrency. Stale versions return 409.
x >= 11
Review type. Validation: required; allowed values: strategy, periodic, annual, stage_change.
strategy, periodic, annual, stage_change "strategy"
Summary. Validation: required, string, max:5000.
5000"Summary"
Outcome. Validation: nullable, string, max:32.
32"Outcome"
curl --request POST \
--url https://api.gratona.com/v1/admin/fundraising/stewardship-plans/{plan_id}/reviews \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"version": 1,
"review_type": "strategy",
"summary": "Summary",
"outcome": "Outcome"
}
'import requests
url = "https://api.gratona.com/v1/admin/fundraising/stewardship-plans/{plan_id}/reviews"
payload = {
"version": 1,
"review_type": "strategy",
"summary": "Summary",
"outcome": "Outcome"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({version: 1, review_type: 'strategy', summary: 'Summary', outcome: 'Outcome'})
};
fetch('https://api.gratona.com/v1/admin/fundraising/stewardship-plans/{plan_id}/reviews', 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://api.gratona.com/v1/admin/fundraising/stewardship-plans/{plan_id}/reviews",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'version' => 1,
'review_type' => 'strategy',
'summary' => 'Summary',
'outcome' => 'Outcome'
]),
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://api.gratona.com/v1/admin/fundraising/stewardship-plans/{plan_id}/reviews"
payload := strings.NewReader("{\n \"version\": 1,\n \"review_type\": \"strategy\",\n \"summary\": \"Summary\",\n \"outcome\": \"Outcome\"\n}")
req, _ := http.NewRequest("POST", 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.post("https://api.gratona.com/v1/admin/fundraising/stewardship-plans/{plan_id}/reviews")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"version\": 1,\n \"review_type\": \"strategy\",\n \"summary\": \"Summary\",\n \"outcome\": \"Outcome\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.gratona.com/v1/admin/fundraising/stewardship-plans/{plan_id}/reviews")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"version\": 1,\n \"review_type\": \"strategy\",\n \"summary\": \"Summary\",\n \"outcome\": \"Outcome\"\n}"
response = http.request(request)
puts response.read_body{
"status": "success",
"data": {
"id": 42,
"reviewed_at": "2026-10-20T12:00:00Z",
"warnings": [
{
"code": "work_projection_delayed",
"message": "Example"
}
]
}
}{
"message": "Unauthorized",
"status": "error"
}{
"message": "Not Found",
"status": "error"
}{
"status": "error",
"code": "PLAN_VERSION_CONFLICT",
"message": "Example",
"data": {
"latest": {
"id": 42,
"version": 2,
"subject": {
"type": "donor",
"id": 42
},
"stage": "qualifying",
"status": "draft",
"tier": "A",
"stage_entered_at": "2026-10-20T12:00:00Z",
"action_donor_id": 42,
"period": {
"start": "2026-10-20",
"end": "2026-10-20"
},
"currency": "USD",
"goal_amount": 1000,
"attributed_actual": 1000,
"direct_actual": 1000,
"soft_credit_actual": 1000,
"variance_amount": 1000,
"gift_count": 2,
"giving_status": "pending",
"giving_last_successful_at": "2026-10-20T12:00:00Z",
"currency_breakdown": [
{
"currency": "Example",
"attributed_actual": 1000,
"direct_actual": 1000,
"soft_credit_actual": 1000,
"gift_count": 2
}
],
"next_move": {
"id": 42,
"title": "Example",
"planned_at": "2026-10-20T12:00:00Z"
},
"open_move_count": 2,
"overdue_move_count": 2,
"health": {
"status": "Example",
"reasons": [
"overdue_move"
]
},
"metric_basis": "relationship_attribution"
},
"conflicting_plan": {}
}
}{
"errors": {},
"status": "error",
"message": "The given data was invalid."
}