Loyalty API
Loyalty is a points-and-tiers rewards program computed on top of the events you’re already sending to the Customer Data Platform — there’s no separate ledger table to sync. Points accrue from a contact’s behavioral CDP events per rules you define (e.g.100 pts for account.created, 1 pt per $1 of order-completed), points burn through redemptions, and a contact’s balance + tier are recomputed on demand from that event history.
Base path: /api/v1/loyalty
Authentication: API key (X-API-Key) or session JWT. Reads require contacts:read; writes require contacts:write.
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/loyalty/program | Get the active program config |
POST | /api/v1/loyalty/program | Create / replace the program config (201) |
PUT | /api/v1/loyalty/program | Update the active program config |
DELETE | /api/v1/loyalty/program | Reset to the default program (204, empty body) |
POST | /api/v1/loyalty/preview | Simulate a program against sample events (no DB write) |
GET | /api/v1/loyalty/members | Paginated list of contacts with loyalty activity |
GET | /api/v1/loyalty/members/{contactId} | One contact’s balance, tier, and traits |
POST | /api/v1/loyalty/members/{contactId}/redeem | Debit points (a redemption) |
POST | /api/v1/loyalty/members/{contactId}/adjust | Operator credit or debit adjustment |
Using the SDKs
import { Orbit } from "@devotel-orbit/node";
const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY! });
// Simulate a program against sample events (no DB write), then read the simulated state
const preview = await orbit.loyalty.preview({
events: [{ event_name: "order-completed", properties: { total: 189.95 } }],
});
console.log(preview.data.state.balance.available, preview.data.state.tier.current.name);
// Redeem points against a member's balance
const member = await orbit.loyalty.getMember("cnt_9f8a7b6c");
const redemption = await orbit.loyalty.redeem("cnt_9f8a7b6c", {
points: 500,
reward: "10% off next order",
reference: "ord_9182",
});
client.request escape hatch runs the same earn → preview → redeem round trip (source-only; vendor it from packages/sdk-python):
from orbit_sdk import OrbitClient
client = OrbitClient.from_api_key("dv_test_sk_YOUR_KEY")
preview = client.request(
"POST",
"/loyalty/preview",
json_body={"events": [{"event_name": "account.created"}, {"event_name": "order-completed", "properties": {"total": 189.95}}]},
)
member = client.request("GET", "/loyalty/members/cnt_9f8a7b6c")
redemption = client.request(
"POST",
"/loyalty/members/cnt_9f8a7b6c/redeem",
json_body={"points": 500, "reward": "10% off next order", "reference": "ord_9182"},
)
ApiResponse envelope. See the SDK index at SDK quickstart.
Program config
A program has earn rules and tiers:perEventrule — a flat point grant every time a named event fires (e.g.50points onreview.submitted).perUnitrule —points = value_in_event_property × pointsPerUnit, rounded per the rule’sroundingmode (e.g.1point per$1of an order’stotal).- Tiers — an ordered ladder (
Bronze→Platinum), each with a lifetime-pointsthresholdand operator-authoredbenefitscopy. The base tier’s threshold is0so every contact always has a current tier. pointsExpiryDays— optional; when set, points lapse this many days after they’re earned. Omit for points that never expire.
curl -X PUT https://api.orbit.devotel.io/api/v1/loyalty/program \
-H "X-API-Key: dv_live_sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "VIP Rewards",
"earnRules": [
{ "id": "signup", "label": "Sign-up bonus", "type": "perEvent", "eventName": "account.created", "points": 100 },
{ "id": "spend", "label": "$1 = 1 point", "type": "perUnit", "eventName": "order-completed", "field": "total", "pointsPerUnit": 1, "rounding": "floor" }
],
"tiers": [
{ "id": "member", "name": "Member", "threshold": 0, "benefits": ["1x points"] },
{ "id": "vip", "name": "VIP", "threshold": 1000, "benefits": ["2x points", "Free shipping"] }
],
"pointsExpiryDays": 365
}'
import { Orbit } from "@devotel-orbit/node";
const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY! });
const res = await orbit.loyalty.updateProgram({
name: "VIP Rewards",
earnRules: [
{ id: "signup", label: "Sign-up bonus", type: "perEvent", eventName: "account.created", points: 100 },
{ id: "spend", label: "$1 = 1 point", type: "perUnit", eventName: "order-completed", field: "total", pointsPerUnit: 1, rounding: "floor" },
],
tiers: [
{ id: "member", name: "Member", threshold: 0, benefits: ["1x points"] },
{ id: "vip", name: "VIP", threshold: 1000, benefits: ["2x points", "Free shipping"] },
],
pointsExpiryDays: 365,
});
console.log(res.data.program_source); // "custom"
# Python has no typed loyalty helper — use the client.request escape hatch.
from orbit_sdk import OrbitClient
client = OrbitClient.from_api_key("dv_test_sk_YOUR_KEY")
res = client.request(
"PUT",
"/loyalty/program",
json_body={
"name": "VIP Rewards",
"earnRules": [
{"id": "signup", "label": "Sign-up bonus", "type": "perEvent", "eventName": "account.created", "points": 100},
{"id": "spend", "label": "$1 = 1 point", "type": "perUnit", "eventName": "order-completed", "field": "total", "pointsPerUnit": 1, "rounding": "floor"},
],
"tiers": [
{"id": "member", "name": "Member", "threshold": 0, "benefits": ["1x points"]},
{"id": "vip", "name": "VIP", "threshold": 1000, "benefits": ["2x points", "Free shipping"]},
],
"pointsExpiryDays": 365,
},
)
print(res["data"]["program_source"]) # custom
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]interface{}{
"name": "VIP Rewards",
"earnRules": []map[string]interface{}{
{"id": "signup", "label": "Sign-up bonus", "type": "perEvent", "eventName": "account.created", "points": 100},
{"id": "spend", "label": "$1 = 1 point", "type": "perUnit", "eventName": "order-completed", "field": "total", "pointsPerUnit": 1, "rounding": "floor"},
},
"tiers": []map[string]interface{}{
{"id": "member", "name": "Member", "threshold": 0, "benefits": []string{"1x points"}},
{"id": "vip", "name": "VIP", "threshold": 1000, "benefits": []string{"2x points", "Free shipping"}},
},
"pointsExpiryDays": 365,
})
req, _ := http.NewRequest(
"PUT",
"https://api.orbit.devotel.io/api/v1/loyalty/program",
bytes.NewReader(body),
)
req.Header.Set("X-API-Key", "dv_live_sk_your_key_here")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var out struct {
Data struct {
ProgramSource string `json:"program_source"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Data.ProgramSource) // custom
}
require "json"
require "net/http"
body = {
name: "VIP Rewards",
earnRules: [
{ id: "signup", label: "Sign-up bonus", type: "perEvent", eventName: "account.created", points: 100 },
{ id: "spend", label: "$1 = 1 point", type: "perUnit", eventName: "order-completed", field: "total", pointsPerUnit: 1, rounding: "floor" },
],
tiers: [
{ id: "member", name: "Member", threshold: 0, benefits: ["1x points"] },
{ id: "vip", name: "VIP", threshold: 1000, benefits: ["2x points", "Free shipping"] },
],
pointsExpiryDays: 365,
}
uri = URI("https://api.orbit.devotel.io/api/v1/loyalty/program")
request = Net::HTTP::Put.new(uri)
request["X-API-Key"] = ENV["ORBIT_API_KEY"]
request["Content-Type"] = "application/json"
request.body = JSON.generate(body)
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
puts JSON.parse(response.body).dig("data", "program_source") # custom
<?php
$key = getenv("ORBIT_API_KEY");
$body = [
"name" => "VIP Rewards",
"earnRules" => [
["id" => "signup", "label" => "Sign-up bonus", "type" => "perEvent", "eventName" => "account.created", "points" => 100],
["id" => "spend", "label" => '$1 = 1 point', "type" => "perUnit", "eventName" => "order-completed", "field" => "total", "pointsPerUnit" => 1, "rounding" => "floor"],
],
"tiers" => [
["id" => "member", "name" => "Member", "threshold" => 0, "benefits" => ["1x points"]],
["id" => "vip", "name" => "VIP", "threshold" => 1000, "benefits" => ["2x points", "Free shipping"]],
],
"pointsExpiryDays" => 365,
];
$ch = curl_init("https://api.orbit.devotel.io/api/v1/loyalty/program");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-API-Key: " . $key,
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $response["data"]["program_source"] . PHP_EOL; // custom
{
"data": {
"program": {
"id": "loy_prog_01",
"name": "VIP Rewards",
"earnRules": [
{ "id": "signup", "label": "Sign-up bonus", "type": "perEvent", "eventName": "account.created", "points": 100 },
{ "id": "spend", "label": "$1 = 1 point", "type": "perUnit", "eventName": "order-completed", "field": "total", "pointsPerUnit": 1, "rounding": "floor" }
],
"tiers": [
{ "id": "member", "name": "Member", "threshold": 0, "benefits": ["1x points"] },
{ "id": "vip", "name": "VIP", "threshold": 1000, "benefits": ["2x points", "Free shipping"] }
],
"pointsExpiryDays": 365
},
"program_source": "custom",
"revision_id": "cdp_evt_01J8QWXYZ",
"redemption_event_name": "loyalty.points_redeemed",
"relevant_event_names": ["loyalty.points_redeemed", "loyalty.points_adjusted", "account.created", "order-completed"]
},
"meta": { "request_id": "req_01J8QWXYZ", "timestamp": "2026-08-26T10:00:00Z" }
}
POST /program (create) and GET /program return this same envelope — create responds 201, read includes program_source: "default" until you author a program. DELETE /program resets to the default and responds 204 with an empty body (the points ledger is untouched — only the config reverts).
Preview before you commit
POST /preview runs the accrual/tier engine against sample events you supply — either your current program or an override — with no database write. Use it to tune earn rules and tier thresholds before adopting them.
curl -X POST https://api.orbit.devotel.io/api/v1/loyalty/preview \
-H "X-API-Key: dv_live_sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"events": [
{ "event_name": "order-completed", "properties": { "total": 250 } },
{ "event_name": "order-completed", "properties": { "total": 90 } }
]
}'
import { Orbit } from "@devotel-orbit/node";
const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY! });
const preview = await orbit.loyalty.preview({
events: [
{ event_name: "order-completed", properties: { total: 250 } },
{ event_name: "order-completed", properties: { total: 90 } },
],
});
console.log(preview.data.state.balance.available); // 340
from orbit_sdk import OrbitClient
client = OrbitClient.from_api_key("dv_test_sk_YOUR_KEY")
preview = client.request(
"POST",
"/loyalty/preview",
json_body={"events": [
{"event_name": "order-completed", "properties": {"total": 250}},
{"event_name": "order-completed", "properties": {"total": 90}},
]},
)
print(preview["data"]["state"]["balance"]["available"]) # 340
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]interface{}{
"events": []map[string]interface{}{
{"event_name": "order-completed", "properties": map[string]interface{}{"total": 250}},
{"event_name": "order-completed", "properties": map[string]interface{}{"total": 90}},
},
})
req, _ := http.NewRequest(
"POST",
"https://api.orbit.devotel.io/api/v1/loyalty/preview",
bytes.NewReader(body),
)
req.Header.Set("X-API-Key", "dv_live_sk_your_key_here")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var out struct {
Data struct {
State struct {
Balance struct {
Available int `json:"available"`
} `json:"balance"`
} `json:"state"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Data.State.Balance.Available) // 340
}
require "json"
require "net/http"
body = {
events: [
{ event_name: "order-completed", properties: { total: 250 } },
{ event_name: "order-completed", properties: { total: 90 } },
],
}
uri = URI("https://api.orbit.devotel.io/api/v1/loyalty/preview")
request = Net::HTTP::Post.new(uri)
request["X-API-Key"] = ENV["ORBIT_API_KEY"]
request["Content-Type"] = "application/json"
request.body = JSON.generate(body)
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
puts JSON.parse(response.body).dig("data", "state", "balance", "available") # 340
<?php
$key = getenv("ORBIT_API_KEY");
$body = [
"events" => [
["event_name" => "order-completed", "properties" => ["total" => 250]],
["event_name" => "order-completed", "properties" => ["total" => 90]],
],
];
$ch = curl_init("https://api.orbit.devotel.io/api/v1/loyalty/preview");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-API-Key: " . $key,
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $response["data"]["state"]["balance"]["available"] . PHP_EOL; // 340
program to run against your saved (or default) program; send program to evaluate a candidate override. Either way the response projects the balance and tier those events would produce for one contact:
{
"data": {
"program_source": "default",
"state": {
"program": "Orbit Rewards",
"balance": {
"available": 340,
"lifetimeEarned": 340,
"totalRedeemed": 0,
"expired": 0,
"expiringSoon": 340,
"nextExpiryAt": "2027-08-26T10:00:00Z"
},
"tier": {
"current": { "id": "member", "name": "Member", "threshold": 0, "benefits": ["1x points"] },
"next": { "id": "vip", "name": "VIP", "threshold": 1000, "benefits": ["2x points", "Free shipping"] },
"pointsToNext": 660,
"progress": 0.34
},
"traits": {
"loyalty_points_balance": 340,
"loyalty_lifetime_points": 340,
"loyalty_tier": "Member",
"loyalty_tier_id": "member"
}
}
},
"meta": { "request_id": "req_01J8QX012", "timestamp": "2026-08-26T10:00:00Z" }
}
Members, balance, and redemption
GET /members lists every contact with loyalty activity, each with a computed balance and tier. Pages use limit (1–100, default 25) and offset as documented in Pagination — the members list is one of the offset-based endpoint families.
curl "https://api.orbit.devotel.io/api/v1/loyalty/members?limit=2&offset=0" \
-H "X-API-Key: dv_live_sk_your_key_here"
import { Orbit } from "@devotel-orbit/node";
const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY! });
const members = await orbit.loyalty.listMembers({ limit: 2, offset: 0 });
for (const member of members.data.members) {
console.log(member.contact_id, member.tier.current.name);
}
from orbit_sdk import OrbitClient
client = OrbitClient.from_api_key("dv_test_sk_YOUR_KEY")
members = client.request("GET", "/loyalty/members?limit=2&offset=0")
for member in members["data"]["members"]:
print(member["contact_id"], member["tier"]["current"]["name"])
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest(
"GET",
"https://api.orbit.devotel.io/api/v1/loyalty/members?limit=2&offset=0",
nil,
)
req.Header.Set("X-API-Key", "dv_live_sk_your_key_here")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var out struct {
Data struct {
Members []struct {
ContactID string `json:"contact_id"`
Tier struct {
Current struct {
Name string `json:"name"`
} `json:"current"`
} `json:"tier"`
} `json:"members"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&out)
for _, member := range out.Data.Members {
fmt.Println(member.ContactID, member.Tier.Current.Name)
}
}
require "json"
require "net/http"
uri = URI("https://api.orbit.devotel.io/api/v1/loyalty/members?limit=2&offset=0")
request = Net::HTTP::Get.new(uri)
request["X-API-Key"] = ENV["ORBIT_API_KEY"]
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
JSON.parse(response.body).dig("data", "members").each do |member|
puts "#{member["contact_id"]} #{member.dig("tier", "current", "name")}"
end
<?php
$key = getenv("ORBIT_API_KEY");
$ch = curl_init("https://api.orbit.devotel.io/api/v1/loyalty/members?limit=2&offset=0");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: " . $key]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
foreach ($response["data"]["members"] as $member) {
echo $member["contact_id"] . " " . $member["tier"]["current"]["name"] . PHP_EOL;
}
{
"data": {
"members": [
{
"contact_id": "cnt_abc123",
"event_count": 14,
"last_activity_at": "2026-08-20T14:12:00Z",
"balance": {
"available": 340,
"lifetimeEarned": 840,
"totalRedeemed": 500,
"expired": 0,
"expiringSoon": 120,
"nextExpiryAt": "2026-09-15T00:00:00Z"
},
"tier": {
"current": { "id": "member", "name": "Member", "threshold": 0, "benefits": ["1x points"] },
"next": { "id": "vip", "name": "VIP", "threshold": 1000, "benefits": ["2x points", "Free shipping"] },
"pointsToNext": 160,
"progress": 0.84
},
"traits": {
"loyalty_points_balance": 340,
"loyalty_lifetime_points": 840,
"loyalty_tier": "Member",
"loyalty_tier_id": "member"
}
}
],
"program_name": "VIP Rewards",
"limit": 2,
"offset": 0
},
"meta": { "request_id": "req_01J8QY345", "timestamp": "2026-08-26T10:00:00Z" }
}
GET /members/{contactId} returns one contact’s full state — available points, lifetimeEarned (drives tier; redemptions never lower it), expiringSoon, and nextExpiryAt.
curl https://api.orbit.devotel.io/api/v1/loyalty/members/cnt_abc123 \
-H "X-API-Key: dv_live_sk_your_key_here"
import { Orbit } from "@devotel-orbit/node";
const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY! });
const member = await orbit.loyalty.getMember("cnt_abc123");
console.log(member.data.balance.available, member.data.tier.current.name);
from orbit_sdk import OrbitClient
client = OrbitClient.from_api_key("dv_test_sk_YOUR_KEY")
member = client.request("GET", "/loyalty/members/cnt_abc123")
print(member["data"]["balance"]["available"], member["data"]["tier"]["current"]["name"])
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest(
"GET",
"https://api.orbit.devotel.io/api/v1/loyalty/members/cnt_abc123",
nil,
)
req.Header.Set("X-API-Key", "dv_live_sk_your_key_here")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var out struct {
Data struct {
Balance struct {
Available int `json:"available"`
} `json:"balance"`
Tier struct {
Current struct {
Name string `json:"name"`
} `json:"current"`
} `json:"tier"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Data.Balance.Available, out.Data.Tier.Current.Name)
}
require "json"
require "net/http"
uri = URI("https://api.orbit.devotel.io/api/v1/loyalty/members/cnt_abc123")
request = Net::HTTP::Get.new(uri)
request["X-API-Key"] = ENV["ORBIT_API_KEY"]
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
member = JSON.parse(response.body)["data"]
puts "#{member.dig("balance", "available")} #{member.dig("tier", "current", "name")}"
<?php
$key = getenv("ORBIT_API_KEY");
$ch = curl_init("https://api.orbit.devotel.io/api/v1/loyalty/members/cnt_abc123");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: " . $key]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $response["data"]["balance"]["available"] . " " . $response["data"]["tier"]["current"]["name"] . PHP_EOL;
{
"data": {
"contact_id": "cnt_abc123",
"program": "VIP Rewards",
"balance": {
"available": 340,
"lifetimeEarned": 840,
"totalRedeemed": 500,
"expired": 0,
"expiringSoon": 120,
"nextExpiryAt": "2026-09-15T00:00:00Z"
},
"tier": {
"current": { "id": "member", "name": "Member", "threshold": 0, "benefits": ["1x points"] },
"next": { "id": "vip", "name": "VIP", "threshold": 1000, "benefits": ["2x points", "Free shipping"] },
"pointsToNext": 160,
"progress": 0.84
},
"traits": {
"loyalty_points_balance": 340,
"loyalty_lifetime_points": 840,
"loyalty_tier": "Member",
"loyalty_tier_id": "member"
}
},
"meta": { "request_id": "req_01J8QZ678", "timestamp": "2026-08-26T10:00:00Z" }
}
POST /members/{contactId}/redeem atomically debits points — concurrent redemptions against the same contact can’t overspend a shared balance. It returns 409 INSUFFICIENT_POINTS when the requested amount exceeds the available balance.
curl -X POST https://api.orbit.devotel.io/api/v1/loyalty/members/cnt_abc123/redeem \
-H "X-API-Key: dv_live_sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "points": 500, "reward": "10% off coupon", "reference": "ord_9182" }'
import { Orbit } from "@devotel-orbit/node";
const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY! });
const redemption = await orbit.loyalty.redeem("cnt_abc123", {
points: 500,
reward: "10% off coupon",
reference: "ord_9182",
});
console.log(redemption.data.debited, redemption.data.balance.available);
from orbit_sdk import OrbitClient
client = OrbitClient.from_api_key("dv_test_sk_YOUR_KEY")
redemption = client.request(
"POST",
"/loyalty/members/cnt_abc123/redeem",
json_body={"points": 500, "reward": "10% off coupon", "reference": "ord_9182"},
)
print(redemption["data"]["debited"], redemption["data"]["balance"]["available"])
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]interface{}{
"points": 500,
"reward": "10% off coupon",
"reference": "ord_9182",
})
req, _ := http.NewRequest(
"POST",
"https://api.orbit.devotel.io/api/v1/loyalty/members/cnt_abc123/redeem",
bytes.NewReader(body),
)
req.Header.Set("X-API-Key", "dv_live_sk_your_key_here")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var out struct {
Data struct {
Debited int `json:"debited"`
Balance struct {
Available int `json:"available"`
} `json:"balance"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Data.Debited, out.Data.Balance.Available)
}
require "json"
require "net/http"
uri = URI("https://api.orbit.devotel.io/api/v1/loyalty/members/cnt_abc123/redeem")
request = Net::HTTP::Post.new(uri)
request["X-API-Key"] = ENV["ORBIT_API_KEY"]
request["Content-Type"] = "application/json"
request.body = JSON.generate(
points: 500,
reward: "10% off coupon",
reference: "ord_9182"
)
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
redemption = JSON.parse(response.body)["data"]
puts "#{redemption["debited"]} #{redemption.dig("balance", "available")}"
<?php
$key = getenv("ORBIT_API_KEY");
$ch = curl_init("https://api.orbit.devotel.io/api/v1/loyalty/members/cnt_abc123/redeem");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-API-Key: " . $key,
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"points" => 500,
"reward" => "10% off coupon",
"reference" => "ord_9182",
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $response["data"]["debited"] . " " . $response["data"]["balance"]["available"] . PHP_EOL;
201 with the post-debit state:
{
"data": {
"contact_id": "cnt_abc123",
"redemption_event_id": "cdp_evt_01J8R0ABC",
"debited": 500,
"program": "VIP Rewards",
"balance": {
"available": 340,
"lifetimeEarned": 840,
"totalRedeemed": 500,
"expired": 0,
"expiringSoon": 120,
"nextExpiryAt": "2026-09-15T00:00:00Z"
},
"tier": {
"current": { "id": "member", "name": "Member", "threshold": 0, "benefits": ["1x points"] },
"next": { "id": "vip", "name": "VIP", "threshold": 1000, "benefits": ["2x points", "Free shipping"] },
"pointsToNext": 160,
"progress": 0.84
},
"traits": {
"loyalty_points_balance": 340,
"loyalty_lifetime_points": 840,
"loyalty_tier": "Member",
"loyalty_tier_id": "member"
}
},
"meta": { "request_id": "req_01J8R1DEF", "timestamp": "2026-08-26T10:00:00Z" }
}
{
"error": {
"code": "INSUFFICIENT_POINTS",
"message": "Insufficient points: available 340, requested 500.",
"status": 409
},
"meta": { "request_id": "req_01J8R2GHI", "timestamp": "2026-08-26T10:00:00Z" }
}
import { Orbit, OrbitApiError } from "@devotel-orbit/node";
const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY! });
try {
await orbit.loyalty.redeem("cnt_abc123", { points: 500, reward: "10% off coupon" });
} catch (error) {
if (error instanceof OrbitApiError && error.code === "INSUFFICIENT_POINTS") {
// error.status === 409; prompt the customer to lower the amount
}
}
from orbit_sdk import OrbitClient, OrbitError
client = OrbitClient.from_api_key("dv_test_sk_YOUR_KEY")
try:
client.request("POST", "/loyalty/members/cnt_abc123/redeem", json_body={"points": 500})
except OrbitError as error:
if error.code == "INSUFFICIENT_POINTS" and error.status == 409:
pass # prompt the customer to lower the amount
curl -s -o response.json -w '%{http_code}' \
-X POST https://api.orbit.devotel.io/api/v1/loyalty/members/cnt_abc123/redeem \
-H "X-API-Key: dv_live_sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "points": 500 }'
# prints 409; response.json carries the error envelope
POST /members/{contactId}/adjust is the operator override: {"direction": "credit", "points": 200, "reason": "Customer service gesture"} grants points directly; {"direction": "debit", ...} removes them through the same overspend-safe path as redemption.
Credit:
curl -X POST https://api.orbit.devotel.io/api/v1/loyalty/members/cnt_abc123/adjust \
-H "X-API-Key: dv_live_sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "direction": "credit", "points": 200, "reason": "Customer service gesture" }'
import { Orbit } from "@devotel-orbit/node";
const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY! });
const adjustment = await orbit.loyalty.adjust("cnt_abc123", {
direction: "credit",
points: 200,
reason: "Customer service gesture",
});
console.log(adjustment.data.points, adjustment.data.balance.available);
from orbit_sdk import OrbitClient
client = OrbitClient.from_api_key("dv_test_sk_YOUR_KEY")
adjustment = client.request(
"POST",
"/loyalty/members/cnt_abc123/adjust",
json_body={"direction": "credit", "points": 200, "reason": "Customer service gesture"},
)
print(adjustment["data"]["points"], adjustment["data"]["balance"]["available"])
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]interface{}{
"direction": "credit",
"points": 200,
"reason": "Customer service gesture",
})
req, _ := http.NewRequest(
"POST",
"https://api.orbit.devotel.io/api/v1/loyalty/members/cnt_abc123/adjust",
bytes.NewReader(body),
)
req.Header.Set("X-API-Key", "dv_live_sk_your_key_here")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var out struct {
Data struct {
Points int `json:"points"`
Balance struct {
Available int `json:"available"`
} `json:"balance"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Data.Points, out.Data.Balance.Available)
}
require "json"
require "net/http"
uri = URI("https://api.orbit.devotel.io/api/v1/loyalty/members/cnt_abc123/adjust")
request = Net::HTTP::Post.new(uri)
request["X-API-Key"] = ENV["ORBIT_API_KEY"]
request["Content-Type"] = "application/json"
request.body = JSON.generate(
direction: "credit",
points: 200,
reason: "Customer service gesture"
)
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(request)
end
adjustment = JSON.parse(response.body)["data"]
puts "#{adjustment["points"]} #{adjustment.dig("balance", "available")}"
<?php
$key = getenv("ORBIT_API_KEY");
$ch = curl_init("https://api.orbit.devotel.io/api/v1/loyalty/members/cnt_abc123/adjust");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-API-Key: " . $key,
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"direction" => "credit",
"points" => 200,
"reason" => "Customer service gesture",
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $response["data"]["points"] . " " . $response["data"]["balance"]["available"] . PHP_EOL;
{
"data": {
"contact_id": "cnt_abc123",
"adjustment_event_id": "cdp_evt_01J8R3JKL",
"direction": "credit",
"points": 200,
"program": "VIP Rewards",
"balance": {
"available": 540,
"lifetimeEarned": 1040,
"totalRedeemed": 500,
"expired": 0,
"expiringSoon": 120,
"nextExpiryAt": "2026-09-15T00:00:00Z"
},
"tier": {
"current": { "id": "vip", "name": "VIP", "threshold": 1000, "benefits": ["2x points", "Free shipping"] },
"next": null,
"pointsToNext": 0,
"progress": 1
},
"traits": {
"loyalty_points_balance": 540,
"loyalty_lifetime_points": 1040,
"loyalty_tier": "VIP",
"loyalty_tier_id": "vip"
}
},
"meta": { "request_id": "req_01J8R4MNO", "timestamp": "2026-08-26T10:00:00Z" }
}
"direction": "debit" with the same points / reason / reference fields. A debit responds 201 with direction: "debit", the deducted points, and the post-debit state; it returns the same 409 INSUFFICIENT_POINTS envelope as redeem when the balance can’t cover the deduction.
Segments and journeys
Every member’s computed state is exposed as traits —loyalty_points_balance, loyalty_lifetime_points, loyalty_tier, loyalty_tier_id — usable directly as entry conditions in Segments and flow triggers, e.g. loyalty_points_balance >= 500.
See also
- Loyalty program guide — step-by-step tutorial for building a program end to end
- CDP API — the event stream loyalty accrues from
- Segments API — build an audience from loyalty traits
- Pagination — page through the members list
- REST API recipes — task-by-task curl and Node cookbook