Use Query parameters to automatically link payment to fund/campaign and appeal?
Comments
-
Hi Nathan,
The Payments API is really more focused on processing the actual payment itself - to manage gift information in Raiser's Edge NXT, you'll want to use the Gift SKY API. For example, when you create a gift record, you can specify an array of "gift split" objects which will include the campaign, fund, appeal (and package) details.0 -
@Ben Lambert
What if my gifts gets do not register campaign ids I am able to pull using a separate get?0 -
@Stew Sheckler
Not exactly sure what your use case is. Can you explain what you trying to accomplish?0 -
@Alex Wong
I'll give you my current code and see if you can tell me why I am getting a 200 OK but I am not pulling any gift/constituent data only getting my Campaign and Fund Data.
What I am trying to do is get all the information from NXT about our Mission Trips for funding and donations and push it to a webportal we are using called ServiceReef. They have specific categories that I am struggling to pull from NXT in an orderly fashion so they will sync.
I hope that makes sense.
<?phpnamespace App\\Services;
use GuzzleHttp\\Client;
use App\\Services\\LogHelper;
use App\\Services\\NXTTokenService;
class NXTService
{
private $constituentCache = [];
private $lastRequestTime = 0;
private $requestsInLastSecond = 0;
private $dailyRequestCount = 0;
private $dailyQuotaReset = 0;
private $maxRequestsPerSecond = 10;
private $maxDailyRequests = 25000; // Standard tier limit
private function handleRateLimiting(\\Exception $e)
{
if ($e->getCode() === 429) { // Rate limit exceeded
$retryAfter = $e->getResponse()->getHeader('Retry-After')[0] ?? 1;
LogHelper::log('INFO', '[NXTService] Rate limit exceeded, waiting...', ['retry_after' => $retryAfter]);
sleep((int)$retryAfter);
return true;
} elseif ($e->getCode() === 403) { // Daily quota exceeded
$retryAfter = $e->getResponse()->getHeader('Retry-After')[0] ?? 3600;
LogHelper::log('ERROR', '[NXTService] Daily quota exceeded', ['retry_after' => $retryAfter]);
throw new \\Exception('Daily API quota exceeded. Please try again later.');
}
return false;
}
private function checkRateLimits()
{
$now = time();
// Reset daily counter if needed (24 hour period)
if ($now - $this->dailyQuotaReset >= 86400) {
$this->dailyRequestCount = 0;
$this->dailyQuotaReset = $now;
}
// Check if we've exceeded daily quota
if ($this->dailyRequestCount >= $this->maxDailyRequests) {
throw new \\Exception('Daily API quota exceeded. Please try again later.');
}
// Handle per-second rate limiting
if ($now === $this->lastRequestTime) {
$this->requestsInLastSecond++;
if ($this->requestsInLastSecond >= $this->maxRequestsPerSecond) {
usleep(100000); // Sleep 100ms if we're at the rate limit
$this->requestsInLastSecond = 0;
$this->lastRequestTime = time();
}
} else {
$this->requestsInLastSecond = 1;
$this->lastRequestTime = $now;
}
$this->dailyRequestCount++;
}
/**
* Cached array of Mission Trip fund IDs
* @var array|null
*/
private $missionTripFundIds = null;
private $client;
private $baseUrl;
private $accessToken;
private $tokenFetchedAt;
private $tokenExpiresIn;
public function __construct()
{
$this->baseUrl = $_ENV['NXT_BASE_URL'] ?? '';
if (empty($this->baseUrl)) {
LogHelper::log('ERROR', '[NXTService] NXT_BASE_URL is missing or empty!', []);
echo "[ERROR] Required environment variable NXT_BASE_URL is missing or empty!\\n";
var_dump($_ENV['NXT_BASE_URL']);
exit(1);
}
LogHelper::log('INFO', 'NXTService NXT_BASE_URL', ['NXT_BASE_URL' => $this->baseUrl]);
$this->loadAccessToken();
$this->initClient();
}
private function loadAccessToken()
{
$tokenService = new NXTTokenService();
LogHelper::log('INFO', '[NXTService] Loading access token from NXTTokenService', []);
$accessToken = $tokenService->getValidAccessToken();
if (!$accessToken) {
LogHelper::log('ERROR', 'NXTService: Failed to load access token from NXTTokenService.', []);
} else {
LogHelper::log('INFO', '[NXTService] Access token loaded successfully', []);
}
$this->accessToken = $accessToken;
$this->tokenFetchedAt = time();
$this->tokenExpiresIn = 3600;
}
private function saveAccessToken($token, $expiresIn)
{
LogHelper::log('INFO', '[NXTService] Saving new access token', ['expires_in' => $expiresIn]);
$this->accessToken = $token;
$this->tokenFetchedAt = time();
$this->tokenExpiresIn = $expiresIn;
// Optionally persist to file or cache if needed
// For now, just update the instance
}
private function initClient()
{
$this->client = new Client([
'base_uri' => $this->baseUrl,
'headers' => [
'Bb-Api-Subscription-Key' => $_ENV['NXT_SUBSCRIPTION_KEY'],
'Authorization' => 'Bearer ' . $this->accessToken,
'Content-Type' => 'application/json',
],
'timeout' => 20, // Increased to 20 seconds to handle larger batches of data
'connect_timeout' => 5 // Keep at 5 seconds as connection issues manifest quickly
]);
LogHelper::log('INFO', '[NXTService] Guzzle Client initialized', [
'base_uri' => $this->baseUrl,
'headers' => [
'Authorization' => 'Bearer ' . $this->accessToken,
'Bb-Api-Subscription-Key' => $_ENV['NXT_SUBSCRIPTION_KEY'],
'Content-Type' => 'application/json',
]
]);
}
private function isTokenExpired()
{
if (!$this->accessToken) return true;
// Consider token expired if within 2 minutes of expiry
return (time() - $this->tokenFetchedAt) > ($this->tokenExpiresIn - 120);
}
private function refreshAccessToken()
{
$tokenService = new NXTTokenService();
LogHelper::log('INFO', '[NXTService] Attempting to refresh Blackbaud NXT access token', []);
try {
$result = $tokenService->refreshAccessToken();
if ($result && isset($result['access_token'])) {
LogHelper::log('INFO', '[NXTService] Access token refreshed successfully', []);
$this->saveAccessToken($result['access_token'], $result['expires_in']);
$this->initClient();
return true;
}
LogHelper::log('ERROR', 'NXTService: Failed to refresh Blackbaud NXT access token - no access_token in response.', []);
} catch (\\Exception $e) {
LogHelper::log('ERROR', 'NXTService: Exception during refreshAccessToken', ['error' => $e->getMessage()]);
return false;
}
}
public function fetchMissionTripDonations()
{
LogHelper::log('INFO', '[NXTService] Starting fetchMissionTripDonations', []);
if ($this->isTokenExpired()) {
$this->refreshAccessToken();
}
try {
$startDate = '2025-01-01T00:00:00-04:00';
$endDate = '2025-12-31T23:59:59-04:00';
// Step 1: Get all campaigns and filter for "Mission Trip"
$campaignsResponse = $this->client->get('/fundraising/v1/campaigns');
$campaigns = json_decode($campaignsResponse->getBody(), true)['value'] ?? [];
// Debug: Log first campaign structure
if (!empty($campaigns)) {
LogHelper::log('DEBUG', '[NXTService] First campaign structure: ' . json_encode($campaigns[0], JSON_PRETTY_PRINT));
}
// Debug: Log all campaign descriptions
$campaignDescs = array_map(function($campaign) {
return $campaign['description'] ?? 'NO_DESCRIPTION';
}, $campaigns);
LogHelper::log('DEBUG', '[NXTService] All campaign descriptions: ' . json_encode($campaignDescs));
$missionTripCampaigns = array_filter($campaigns, function ($campaign) {
return ($campaign['description'] ?? '') === 'Mission Trips' ||
($campaign['description'] ?? '') === '2025 Mission Trips';
});
$campaignIds = array_column($missionTripCampaigns, 'id');
// Step 2: Get all funds and filter for "Mission Trip"
$fundsResponse = $this->client->get('/fundraising/v1/funds');
$funds = json_decode($fundsResponse->getBody(), true)['value'] ?? [];
// Debug: Log first fund structure
if (!empty($funds)) {
LogHelper::log('DEBUG', '[NXTService] First fund structure: ' . json_encode($funds[0], JSON_PRETTY_PRINT));
}
// Debug: Log all fund descriptions
$fundDescs = array_map(function($fund) {
return $fund['description'] ?? 'NO_DESCRIPTION';
}, $funds);
LogHelper::log('DEBUG', '[NXTService] All fund descriptions: ' . json_encode($fundDescs));
$missionTripFunds = array_filter($funds, function ($fund) {
$desc = $fund['description'] ?? '';
return strpos($desc, 'Mission Trip :') === 0;
});
$fundIds = array_column($missionTripFunds, 'id');
LogHelper::log('INFO', 'Filtered campaigns: ' . json_encode($campaignIds));
LogHelper::log('INFO', 'Filtered funds: ' . json_encode($fundIds));
// Step 3: Fetch all gifts in 2025
$offset = 0;
$limit = 100;
$gifts = [];
do {
// Build campaign filter
$campaignFilter = count($campaignIds) > 0 ?
'campaign_id in (' . implode(',', $campaignIds) . ')' : 'false';
// Build fund filter
$fundFilter = count($fundIds) > 0 ?
'fund_id in (' . implode(',', $fundIds) . ')' : 'false';
$response = $this->client->get('/gift/v1/gifts', [
'query' => [
'filter' => "(date ge '$startDate' and date le '$endDate') and (($campaignFilter) or ($fundFilter))",
'skip' => $offset,
'limit' => 100,
'include' => 'constituent_id,amount,date,lookup_id,payment_method,payments,gift_splits,gift_status,is_anonymous,fund,campaign'
]
]);
$batch = json_decode($response->getBody(), true)['value'] ?? [];
foreach ($batch as $gift) {
$gifts[] = $gift;
}
LogHelper::log('INFO', '[NXTService] Processed gifts page', [
'page_size' => count($batch),
'total_filtered_gifts' => count($gifts),
'page' => ($offset / $limit) + 1
]);
$offset += $limit;
} while (count($batch) === $limit);
LogHelper::log('INFO', 'Filtered mission trip gifts count: ' . count($gifts));
return $gifts;
} catch (\\Exception $e) {
LogHelper::log('ERROR', '[NXTService] Error in fetchMissionTripDonations', [
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* Fetch and cache the fund IDs that are categorized as "Mission Trip".
* Returns an array of fund IDs.
*/
private function getMissionTripFundIds()
{
// Use cached list if available
if ($this->missionTripFundIds !== null) {
LogHelper::log('DEBUG', '[NXTService] Using cached mission trip fund IDs', [
'count' => count($this->missionTripFundIds)
]);
return $this->missionTripFundIds;
}
try {
// Get all funds and filter for "Mission Trip"
$fundsResponse = $this->client->get('/fundraising/v1/funds');
$funds = json_decode($fundsResponse->getBody(), true)['value'] ?? [];
$missionTripFunds = array_filter($funds, function ($fund) {
return stripos($fund['description'] ?? '', 'mission trip') !== false;
});
$this->missionTripFundIds = array_column($missionTripFunds, 'id');
LogHelper::log('INFO', '[NXTService] Found mission trip funds', [
'count' => count($this->missionTripFundIds),
'fund_ids' => $this->missionTripFundIds
]);
return $this->missionTripFundIds;
} catch (\\Exception $e) {
LogHelper::log('ERROR', '[NXTService] Error fetching mission trip funds', [
'error' => $e->getMessage()
]);
return [];
}
}
/**
* Determine if a gift is for a Mission Trip fund.
*/
private function isMissionTripGift($gift)
{
$fundId = $gift['fund']['id'] ?? null;
if ($fundId) {
$missionTripFundIds = $this->getMissionTripFundIds();
$isMissionTrip = in_array($fundId, $missionTripFundIds);
LogHelper::log('DEBUG', '[NXTService] Gift fund ID isMissionTrip', [
'fund_id' => $fundId,
'is_mission_trip' => $isMissionTrip
]);
return $isMissionTrip;
}
return false;
}
/**
* Transform a Blackbaud NXT gift record to a Service Reef-compatible donation object.
* Includes: Event, Constituent info, Gift type, Gift amount, Gift Date, Fund Name, ID number, and Contact info.
*/
private function getCo
0 -
@Stew Sheckler
I don't normally help people do code review like this, it takes up a lot of time just to read through a big chunk of code. So this is one time only…you are “trying” to call the Get Gift List API call with start_gift_date, end_gift_date, campaign_id, and fund_id query parameter, however:
- your code is using ODATA Query: (date ge '$startDate' and date le '$endDate') and (($campaignFilter) or ($fundFilter))
- this is not how you filter with SKY API, you will want to put in debug code to see what is the request object looks like, it should have the query paramter looking like this: start_gift_date=2025-01-01&end_gift_date=2025-12-31&campaign_id=1&campaign_id=2&campaign_id=10
- Get Gift List API does not do OR condition, query parameter is all AND condition, that's why in my sample above, I did not include the fund_id parameter, as it will only add another AND condition to your pull
If you want to do complex and/or filtering, consider using Query API instead
0 - your code is using ODATA Query: (date ge '$startDate' and date le '$endDate') and (($campaignFilter) or ($fundFilter))
Categories
- All Categories
- 6 Blackbaud Community Help
- 206 bbcon®
- 1.4K Blackbaud Altru®
- 394 Blackbaud Award Management™ and Blackbaud Stewardship Management™
- 1.1K Blackbaud CRM™ and Blackbaud Internet Solutions™
- 15 donorCentrics®
- 357 Blackbaud eTapestry®
- 2.5K Blackbaud Financial Edge NXT®
- 646 Blackbaud Grantmaking™
- 561 Blackbaud Education Management Solutions for Higher Education
- 3.2K Blackbaud Education Management Solutions for K-12 Schools
- 934 Blackbaud Luminate Online® and Blackbaud TeamRaiser®
- 84 JustGiving® from Blackbaud®
- 6.4K Blackbaud Raiser's Edge NXT®
- 3.6K SKY Developer
- 242 ResearchPoint™
- 118 Blackbaud Tuition Management™
- 165 Organizational Best Practices
- 238 The Tap (Just for Fun)
- 33 Blackbaud Community Challenges
- 28 PowerUp Challenges
- 3 (Open) Raiser's Edge NXT PowerUp Challenge: Product Update Briefing
- 3 (Closed) Raiser's Edge NXT PowerUp Challenge: Standard Reports+
- 3 (Closed) Raiser's Edge NXT PowerUp Challenge: Email Marketing
- 3 (Closed) Raiser's Edge NXT PowerUp Challenge: Gift Management
- 4 (Closed) Raiser's Edge NXT PowerUp Challenge: Event Management
- 3 (Closed) Raiser's Edge NXT PowerUp Challenge: Home Page
- 4 (Closed) Raiser's Edge NXT PowerUp Challenge: Standard Reports
- 4 (Closed) Raiser's Edge NXT PowerUp Challenge: Query
- 778 Community News
- 2.9K Jobs Board
- 53 Blackbaud SKY® Reporting Announcements
- 47 Blackbaud CRM Higher Ed Product Advisory Group (HE PAG)
- 19 Blackbaud CRM Product Advisory Group (BBCRM PAG)


