Track and Consume Credits
curl --request POST \
--url https://api.nozle.app/engine/api/v1/usage/track \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.nozle.app/engine/api/v1/usage/track"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.nozle.app/engine/api/v1/usage/track', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.nozle.app/engine/api/v1/usage/track"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}require 'uri'
require 'net/http'
url = URI("https://api.nozle.app/engine/api/v1/usage/track")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.nozle.app/engine/api/v1/usage/track",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}HttpResponse<String> response = Unirest.post("https://api.nozle.app/engine/api/v1/usage/track")
.header("Authorization", "Bearer <token>")
.asString();Product Credits
Track and Consume Credits
Atomically consume credits and queue the matching billing event
POST
/
engine
/
api
/
v1
/
usage
/
track
Track and Consume Credits
curl --request POST \
--url https://api.nozle.app/engine/api/v1/usage/track \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.nozle.app/engine/api/v1/usage/track"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.nozle.app/engine/api/v1/usage/track', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.nozle.app/engine/api/v1/usage/track"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}require 'uri'
require 'net/http'
url = URI("https://api.nozle.app/engine/api/v1/usage/track")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.nozle.app/engine/api/v1/usage/track",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}HttpResponse<String> response = Unirest.post("https://api.nozle.app/engine/api/v1/usage/track")
.header("Authorization", "Bearer <token>")
.asString();Base URL:
Insufficient credits return
https://api.nozle.app/engine/api/v1.
Auth: secret key only. The customer, Feature, and optional Credit System must belong to the authenticated organization. When your organization has API key permissions, the key requires credit_system:write; a key without it returns 403 permission_denied.
POST /usage/track
Authorization: Bearer sk_nozle_...
Idempotency-Key: execution_0183f
Content-Type: application/json
{
"customer_id": "customer_123",
"feature_code": "agent_execution",
"credit_system_code": "ai_credits",
"subscription_id": "sub_pro_123",
"properties": {},
"timestamp": "2026-07-20T12:00:00.750Z"
}
customer_id and feature_code are required. Add entity_id to attribute the usage to one Entity of the customer and spend under its pool policy. Add subscription_id to choose the subscription that pays; see Which subscription pays. Unknown fields return 400 invalid_request. An unknown Entity returns 404 entity_not_found, an inactive Entity returns 422 entity_inactive, and Entity usage without an explicit pool policy on the plan returns 422 credit_pool_policy_not_configured.
Which subscription pays
Nozle picks the subscription that pays for the usage, and that receives the billing event, in this order:subscription_idgiven: that active subscription of the customer, by its external id. A subscription that is not active, not the customer’s, or (withentity_id) owned by another Entity returns404 active_subscription_not_found.entity_idgiven: the Entity’s own active subscription, else the customer’s own active subscription (one not owned by an Entity). Another Entity’s subscription never pays.- Neither: the customer’s one active subscription.
422 active_subscription_ambiguous, and the message names subscription_id and entity_id. Send one of them to choose.
A customer with no active subscription can still spend credits that do not need one: paid top-ups, opening balances, and plan credits left after a cancellation until they expire. That usage sends no billing event, because usage is billed only through a subscription.
subscription_id is part of the request body, so a replay with the same Idempotency-Key must send the same value.
Response
A successful response includes the operation ID, exact credits consumed, remaining total, and source deductions with their balance-source IDs:{
"allowed": true,
"operation_id": "8d3c2b1a-0f9e-4d8c-b7a6-5e4d3c2b1a09",
"metric_amount": "1",
"credit_system": "ai_credits",
"credits_consumed": "2",
"remaining": "623",
"deductions": [
{
"balance_source_id": "bf659149-0d70-4d2a-8a7b-a4569cc742eb",
"source_type": "subscription_grant",
"source_scope": "customer",
"amount": "2",
"remaining": "623"
}
]
}
200 with allowed=false, credits_required,
available, and reason, without allocations or a Core event. entity_id
and pool_policy are included when they apply. Empty fields are omitted.
Amount rounding and Features
A consumed amount that does not end within 12 decimals is rounded up to the 12th decimal, so usage is never under-charged: 1 event at 1 credit per 3 events consumes0.333333333334, and 7 seconds at 1 credit per 60 seconds consumes 0.116666666667. Exact amounts are unchanged. Before, such rates answered 422 credit_usage_configuration_invalid. A boolean or config Feature code answers 422 feature_not_consumable, not a server error.
Timestamp and retries
timestamp is required. A missing or empty value returns 400 invalid_request with timestamp is required; the API does not default it. The Node.js, Python and Go SDKs fill it with the current time when you omit it, and reuse that time when the same idempotency key is sent again on the same client. It must be Unix seconds (a JSON number such as 1788345001 or 1788345001.5, or a numeric string such as "1788345001") or an RFC 3339 string with a time zone offset or Z. A date without a time, a time without an offset, zero or negative seconds, and any other JSON type return 400 invalid_request with timestamp must be Unix seconds (a number or a numeric string) or an RFC 3339 timestamp with an offset or Z (zero or negative seconds: timestamp must be greater than zero). Unix seconds are available from the Engine release that includes this change; before it, only RFC 3339 is accepted. RFC 3339 keeps its nanoseconds, so a replay must send the same value; the same instant sent in another form is a different request body. The time must also fall after the Credit System rate became effective; an earlier time returns 422 with code credit_usage_configuration_invalid and the message no effective credit rate.
Idempotency-Key is required and must not exceed 255 bytes; otherwise the request returns 400 invalid_request. Retry HTTP 503 with the same idempotency key and the same body, including the same timestamp. The 503 codes are credit_usage_contention (Retry-After: 1), credit_usage_publication_backlog (Retry-After: 30), and credit_usage_temporarily_unavailable (Retry-After: 1). credit_usage_publication_backlog means the organization has 1,000 or more usage events still being retried, or the oldest one is older than 5 minutes. It clears by itself once the events are accepted again. A dead-lettered event, one that failed after about a day of retries, does not block usage and does not count toward either limit; Nozle support requeues it. A replay returns the original result without consuming credits again, and that includes an allowed=false denial. Reusing a key with a different body returns 409 with {"code": "idempotency_key_reused"}. The SDKs remember the time they filled in only on the same client, for the last 1000 keys; a retry from another process or after a restart must send the stored time, so store it once per unit of work.
For staged validation and canary guidance, see Credit Rollout and Acceptance.