curl --request POST \
--url https://api.unprice.dev/v1/budgets/upsert \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"key": "customer:acme",
"unit": "USD",
"limits": [
{
"amount": "0.05",
"window": "month",
"id": "month",
"anchor": 1792022400000,
"enforce": "hard"
}
],
"closesAt": 1798761599000,
"holds": {
"defaultTtlMs": 900000,
"maxTtlMs": 86400000
},
"onUnavailable": "deny"
}
'import requests
url = "https://api.unprice.dev/v1/budgets/upsert"
payload = {
"key": "customer:acme",
"unit": "USD",
"limits": [
{
"amount": "0.05",
"window": "month",
"id": "month",
"anchor": 1792022400000,
"enforce": "hard"
}
],
"closesAt": 1798761599000,
"holds": {
"defaultTtlMs": 900000,
"maxTtlMs": 86400000
},
"onUnavailable": "deny"
}
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({
key: 'customer:acme',
unit: 'USD',
limits: [
{
amount: '0.05',
window: 'month',
id: 'month',
anchor: 1792022400000,
enforce: 'hard'
}
],
closesAt: 1798761599000,
holds: {defaultTtlMs: 900000, maxTtlMs: 86400000},
onUnavailable: 'deny'
})
};
fetch('https://api.unprice.dev/v1/budgets/upsert', 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.unprice.dev/v1/budgets/upsert",
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([
'key' => 'customer:acme',
'unit' => 'USD',
'limits' => [
[
'amount' => '0.05',
'window' => 'month',
'id' => 'month',
'anchor' => 1792022400000,
'enforce' => 'hard'
]
],
'closesAt' => 1798761599000,
'holds' => [
'defaultTtlMs' => 900000,
'maxTtlMs' => 86400000
],
'onUnavailable' => 'deny'
]),
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.unprice.dev/v1/budgets/upsert"
payload := strings.NewReader("{\n \"key\": \"customer:acme\",\n \"unit\": \"USD\",\n \"limits\": [\n {\n \"amount\": \"0.05\",\n \"window\": \"month\",\n \"id\": \"month\",\n \"anchor\": 1792022400000,\n \"enforce\": \"hard\"\n }\n ],\n \"closesAt\": 1798761599000,\n \"holds\": {\n \"defaultTtlMs\": 900000,\n \"maxTtlMs\": 86400000\n },\n \"onUnavailable\": \"deny\"\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.unprice.dev/v1/budgets/upsert")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"key\": \"customer:acme\",\n \"unit\": \"USD\",\n \"limits\": [\n {\n \"amount\": \"0.05\",\n \"window\": \"month\",\n \"id\": \"month\",\n \"anchor\": 1792022400000,\n \"enforce\": \"hard\"\n }\n ],\n \"closesAt\": 1798761599000,\n \"holds\": {\n \"defaultTtlMs\": 900000,\n \"maxTtlMs\": 86400000\n },\n \"onUnavailable\": \"deny\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.unprice.dev/v1/budgets/upsert")
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 \"key\": \"customer:acme\",\n \"unit\": \"USD\",\n \"limits\": [\n {\n \"amount\": \"0.05\",\n \"window\": \"month\",\n \"id\": \"month\",\n \"anchor\": 1792022400000,\n \"enforce\": \"hard\"\n }\n ],\n \"closesAt\": 1798761599000,\n \"holds\": {\n \"defaultTtlMs\": 900000,\n \"maxTtlMs\": 86400000\n },\n \"onUnavailable\": \"deny\"\n}"
response = http.request(request)
puts response.read_body{
"changes": [
{
"change": "limit.amount",
"limitId": "<string>",
"from": "<string>",
"to": "<string>"
}
],
"budget": {
"key": "<string>",
"unit": "money:USD",
"version": 123,
"limits": [
{
"id": "<string>",
"amount": "<string>",
"window": "month",
"enforce": "hard",
"anchor": 123
}
],
"holds": {
"defaultTtlMs": 123,
"maxTtlMs": 123
},
"onUnavailable": "deny",
"closesAt": 123,
"closed": true,
"balance": {
"remaining": "<string>",
"held": "<string>",
"limits": [
{
"id": "<string>",
"window": "month",
"enforce": "hard",
"amount": "<string>",
"consumed": "<string>",
"held": "<string>",
"remaining": "<string>",
"resetsAt": 123
}
],
"asOf": 123
},
"operations": {
"upsert": 1,
"authorize": 1,
"settle": 1,
"release": 1,
"charge": 1,
"record": 1,
"lease": 1,
"report": 1
}
}
}{
"error": {
"code": "BAD_REQUEST",
"docs": "https://docs.unprice.dev/api-reference/errors/code/BAD_REQUEST",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "UNAUTHORIZED",
"docs": "https://docs.unprice.dev/api-reference/errors/code/UNAUTHORIZED",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "FORBIDDEN",
"docs": "https://docs.unprice.dev/api-reference/errors/code/FORBIDDEN",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "NOT_FOUND",
"docs": "https://docs.unprice.dev/api-reference/errors/code/NOT_FOUND",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "CONFLICT",
"docs": "https://docs.unprice.dev/api-reference/errors/code/CONFLICT",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "PRECONDITION_FAILED",
"docs": "https://docs.unprice.dev/api-reference/errors/code/PRECONDITION_FAILED",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "PAYLOAD_TOO_LARGE",
"docs": "https://docs.unprice.dev/api-reference/errors/code/PAYLOAD_TOO_LARGE",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "TOO_MANY_REQUESTS",
"docs": "https://docs.unprice.dev/api-reference/errors/code/TOO_MANY_REQUESTS",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "INTERNAL_SERVER_ERROR",
"docs": "https://docs.unprice.dev/api-reference/errors/code/INTERNAL_SERVER_ERROR",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}create or update a budget
Create the budget at a key, or replace its definition. Every upsert sends the whole definition: a field it leaves out takes its default, so an omitted closesAt reopens a closing budget and omitted hold TTLs return to their defaults. Sending the same definition again changes nothing. The unit cannot change after creation. A hard cap below current consumption plus outstanding reservations returns 409. A runtime key can create a budget or tighten it; raising or removing a hard limit, making it soft, or clearing or postponing closesAt needs a config key and otherwise returns 403, so whoever spends under a cap cannot lift it.
curl --request POST \
--url https://api.unprice.dev/v1/budgets/upsert \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"key": "customer:acme",
"unit": "USD",
"limits": [
{
"amount": "0.05",
"window": "month",
"id": "month",
"anchor": 1792022400000,
"enforce": "hard"
}
],
"closesAt": 1798761599000,
"holds": {
"defaultTtlMs": 900000,
"maxTtlMs": 86400000
},
"onUnavailable": "deny"
}
'import requests
url = "https://api.unprice.dev/v1/budgets/upsert"
payload = {
"key": "customer:acme",
"unit": "USD",
"limits": [
{
"amount": "0.05",
"window": "month",
"id": "month",
"anchor": 1792022400000,
"enforce": "hard"
}
],
"closesAt": 1798761599000,
"holds": {
"defaultTtlMs": 900000,
"maxTtlMs": 86400000
},
"onUnavailable": "deny"
}
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({
key: 'customer:acme',
unit: 'USD',
limits: [
{
amount: '0.05',
window: 'month',
id: 'month',
anchor: 1792022400000,
enforce: 'hard'
}
],
closesAt: 1798761599000,
holds: {defaultTtlMs: 900000, maxTtlMs: 86400000},
onUnavailable: 'deny'
})
};
fetch('https://api.unprice.dev/v1/budgets/upsert', 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.unprice.dev/v1/budgets/upsert",
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([
'key' => 'customer:acme',
'unit' => 'USD',
'limits' => [
[
'amount' => '0.05',
'window' => 'month',
'id' => 'month',
'anchor' => 1792022400000,
'enforce' => 'hard'
]
],
'closesAt' => 1798761599000,
'holds' => [
'defaultTtlMs' => 900000,
'maxTtlMs' => 86400000
],
'onUnavailable' => 'deny'
]),
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.unprice.dev/v1/budgets/upsert"
payload := strings.NewReader("{\n \"key\": \"customer:acme\",\n \"unit\": \"USD\",\n \"limits\": [\n {\n \"amount\": \"0.05\",\n \"window\": \"month\",\n \"id\": \"month\",\n \"anchor\": 1792022400000,\n \"enforce\": \"hard\"\n }\n ],\n \"closesAt\": 1798761599000,\n \"holds\": {\n \"defaultTtlMs\": 900000,\n \"maxTtlMs\": 86400000\n },\n \"onUnavailable\": \"deny\"\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.unprice.dev/v1/budgets/upsert")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"key\": \"customer:acme\",\n \"unit\": \"USD\",\n \"limits\": [\n {\n \"amount\": \"0.05\",\n \"window\": \"month\",\n \"id\": \"month\",\n \"anchor\": 1792022400000,\n \"enforce\": \"hard\"\n }\n ],\n \"closesAt\": 1798761599000,\n \"holds\": {\n \"defaultTtlMs\": 900000,\n \"maxTtlMs\": 86400000\n },\n \"onUnavailable\": \"deny\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.unprice.dev/v1/budgets/upsert")
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 \"key\": \"customer:acme\",\n \"unit\": \"USD\",\n \"limits\": [\n {\n \"amount\": \"0.05\",\n \"window\": \"month\",\n \"id\": \"month\",\n \"anchor\": 1792022400000,\n \"enforce\": \"hard\"\n }\n ],\n \"closesAt\": 1798761599000,\n \"holds\": {\n \"defaultTtlMs\": 900000,\n \"maxTtlMs\": 86400000\n },\n \"onUnavailable\": \"deny\"\n}"
response = http.request(request)
puts response.read_body{
"changes": [
{
"change": "limit.amount",
"limitId": "<string>",
"from": "<string>",
"to": "<string>"
}
],
"budget": {
"key": "<string>",
"unit": "money:USD",
"version": 123,
"limits": [
{
"id": "<string>",
"amount": "<string>",
"window": "month",
"enforce": "hard",
"anchor": 123
}
],
"holds": {
"defaultTtlMs": 123,
"maxTtlMs": 123
},
"onUnavailable": "deny",
"closesAt": 123,
"closed": true,
"balance": {
"remaining": "<string>",
"held": "<string>",
"limits": [
{
"id": "<string>",
"window": "month",
"enforce": "hard",
"amount": "<string>",
"consumed": "<string>",
"held": "<string>",
"remaining": "<string>",
"resetsAt": 123
}
],
"asOf": 123
},
"operations": {
"upsert": 1,
"authorize": 1,
"settle": 1,
"release": 1,
"charge": 1,
"record": 1,
"lease": 1,
"report": 1
}
}
}{
"error": {
"code": "BAD_REQUEST",
"docs": "https://docs.unprice.dev/api-reference/errors/code/BAD_REQUEST",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "UNAUTHORIZED",
"docs": "https://docs.unprice.dev/api-reference/errors/code/UNAUTHORIZED",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "FORBIDDEN",
"docs": "https://docs.unprice.dev/api-reference/errors/code/FORBIDDEN",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "NOT_FOUND",
"docs": "https://docs.unprice.dev/api-reference/errors/code/NOT_FOUND",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "CONFLICT",
"docs": "https://docs.unprice.dev/api-reference/errors/code/CONFLICT",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "PRECONDITION_FAILED",
"docs": "https://docs.unprice.dev/api-reference/errors/code/PRECONDITION_FAILED",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "PAYLOAD_TOO_LARGE",
"docs": "https://docs.unprice.dev/api-reference/errors/code/PAYLOAD_TOO_LARGE",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "TOO_MANY_REQUESTS",
"docs": "https://docs.unprice.dev/api-reference/errors/code/TOO_MANY_REQUESTS",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}{
"error": {
"code": "INTERNAL_SERVER_ERROR",
"docs": "https://docs.unprice.dev/api-reference/errors/code/INTERNAL_SERVER_ERROR",
"message": "<string>",
"requestId": "req_1234",
"details": {
"kind": "invalid_config",
"issues": [
{
"path": "config.plans[0].version.features[1].featureSlug",
"message": "<string>"
}
]
}
}
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
The upsert request
The budget's key, chosen by you and unique within the project. Up to 128 letters, digits, and : / _ . @ -, starting with a letter or digit.
^[A-Za-z0-9][A-Za-z0-9:/_.@-]{0,127}$"customer:acme"
A currency code (shorthand for money:), credits:, or units:
^([A-Z]{3}|money:[A-Z]{3}|credits:[a-z0-9_-]{1,64}|units:[a-z0-9_-]{1,64})$"USD"
Up to 16 limits. Every hard limit must have room for a hold or charge. An empty list makes a budget that only counts.
16Show child attributes
Show child attributes
When the budget closes, in epoch milliseconds. From then on authorize and charge return 409, record is accepted for 1 day more, and settle and release keep working. 30 days after closesAt, once nothing is left to settle or replay, the budget is deleted: get returns 404 and an upsert creates it anew. An upsert replaces the whole definition, so omitting closesAt or sending null leaves the budget open and clears any close time it had. A runtime key cannot close a budget in the past: an earlier closesAt takes effect now.
x >= 01798761599000
Hold timing. Both TTLs must be between 1000 and 86400000 ms, and the default must not exceed the max. An omitted TTL takes its default on every upsert.
Show child attributes
Show child attributes
Deny new work when Unprice cannot be reached. Defaults to deny.
deny "deny"