Submit a Snapshot
curl --request PUT \
--url https://getbill.io/external-api/v1/campaigns/{apiReference}/snapshots/{snapshotReference} \
--header 'Authorization: Bearer <token>'import requests
url = "https://getbill.io/external-api/v1/campaigns/{apiReference}/snapshots/{snapshotReference}"
headers = {"Authorization": "Bearer <token>"}
response = requests.put(url, headers=headers)
print(response.text)const options = {method: 'PUT', headers: {Authorization: 'Bearer <token>'}};
fetch('https://getbill.io/external-api/v1/campaigns/{apiReference}/snapshots/{snapshotReference}', 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://getbill.io/external-api/v1/campaigns/{apiReference}/snapshots/{snapshotReference}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
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;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://getbill.io/external-api/v1/campaigns/{apiReference}/snapshots/{snapshotReference}"
req, _ := http.NewRequest("PUT", 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))
}HttpResponse<String> response = Unirest.put("https://getbill.io/external-api/v1/campaigns/{apiReference}/snapshots/{snapshotReference}")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://getbill.io/external-api/v1/campaigns/{apiReference}/snapshots/{snapshotReference}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_bodyCampaigns API
Submit a Snapshot
Submit a complete snapshot of at most 20,000 rows
PUT
/
external-api
/
v1
/
campaigns
/
{apiReference}
/
snapshots
/
{snapshotReference}
Submit a Snapshot
curl --request PUT \
--url https://getbill.io/external-api/v1/campaigns/{apiReference}/snapshots/{snapshotReference} \
--header 'Authorization: Bearer <token>'import requests
url = "https://getbill.io/external-api/v1/campaigns/{apiReference}/snapshots/{snapshotReference}"
headers = {"Authorization": "Bearer <token>"}
response = requests.put(url, headers=headers)
print(response.text)const options = {method: 'PUT', headers: {Authorization: 'Bearer <token>'}};
fetch('https://getbill.io/external-api/v1/campaigns/{apiReference}/snapshots/{snapshotReference}', 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://getbill.io/external-api/v1/campaigns/{apiReference}/snapshots/{snapshotReference}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
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;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://getbill.io/external-api/v1/campaigns/{apiReference}/snapshots/{snapshotReference}"
req, _ := http.NewRequest("PUT", 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))
}HttpResponse<String> response = Unirest.put("https://getbill.io/external-api/v1/campaigns/{apiReference}/snapshots/{snapshotReference}")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://getbill.io/external-api/v1/campaigns/{apiReference}/snapshots/{snapshotReference}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_bodyThis is a complete snapshot. After asynchronous processing, every active campaign debt omitted from
rows is treated as settled.Authentication and limits
Requiresdebts:write. Uses the batch limiter: 50 requests/hour per authenticated company. The body must be at most 10 MiB and contain at most 20,000 rows.
string
required
Your campaign reference.
string
required
Your client-created, stable reference for this complete logical snapshot. Use an immutable export ID or UUID; do not use a date alone. Generate a reference. Reuse it only to retry the same content.
array
required
Complete snapshot rows. Choose the balance granularity before sending rows.
rows. Its accepted fields are defined in the snapshot row contract; other fields are rejected, except that the legacy accept_expensive_destination key is tolerated and ignored regardless of its value or JSON type.
A row may include a payment_plan object to send every installment of a plan in one operation. See Send an external payment plan for activation, cancellation, and idempotency rules.
Use invoice_reference for one invoice balance per row, or internal_id for one account balance per row, as described in the guide. The snapshot itself communicates balance changes; do not send payment events separately. When both are included, internal_id supplies the primary identity, and invoice_reference is also validated and must not belong to another debt; integrations should normally send only their chosen representation.
Balance and payment-plan behavior
The last accepted API call wins.remaining_amount is the current unpaid balance, whether it moves down or up. A present payment_plan replaces any active plan local to that debt when the total of its pending installments matches remaining_amount. A mismatch returns invalid_snapshot_row on the payment_plan field. Omitting payment_plan keeps the active local payment plan unchanged. Every installment with status set to paid requires paid_at and an external_payment_id that is unique for the authenticated integration and debt.
The fields snapshot_at, allocations, financial_operations, coverage, and covered_remaining_amount are not accepted. Request receipt time provides the audit chronology.
curl -X PUT "https://getbill.io/external-api/v1/campaigns/client-a-2026/snapshots/export-2026-07-17-550e8400" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
--data-binary '{
"rows": [
{
"invoice_reference": "FAC-2026-0042",
"remaining_amount": 249.9,
"currency": "EUR",
"firstname": "Amina",
"lastname": "Martin",
"email": "amina@example.com",
"payment_plan": {
"external_reference": "plan-789",
"status": "active",
"installments": [
{"external_reference": "installment-1", "amount": 100, "payment_date": "2026-09-01", "status": "pending"},
{"external_reference": "installment-2", "amount": 149.9, "payment_date": "2026-10-01", "status": "pending"}
]
}
}
]
}'
HTTP/1.1 202 Accepted
Location: /external-api/v1/campaigns/client-a-2026/snapshots/export-2026-07-17-550e8400
{
"error": false,
"message": "Snapshot accepted",
"data": {
"api_reference": "client-a-2026",
"snapshot_reference": "export-2026-07-17-550e8400",
"delivery_mode": "one_request",
"status": "pending",
"core_applied": false,
"expected_row_count": 1,
"received_row_count": 1,
"received_chunk_count": 1,
"next_chunk_index": 1,
"counts": null,
"failure": null,
"created_at": "2026-07-17T09:00:00Z",
"updated_at": "2026-07-17T09:00:00Z",
"status_url": "/external-api/v1/campaigns/client-a-2026/snapshots/export-2026-07-17-550e8400"
}
}
status_url; 202 does not mean processing is complete.
Costly phone destinations follow the GetBill campaign configuration, which defaults to disabled and cannot be changed by the partner request. They are reported after asynchronous processing in the status resource as non-blocking warnings; they do not reject the complete snapshot or remove a row from full_sync reconciliation. See Get Snapshot Status.
Idempotent retry
After a timeout, retry the exact request-body bytes. GetBill compares the delivery mode and a server-computed SHA-256 of the raw body. JSON with different whitespace or key order conflicts even when it decodes to the same data. An identical retry returns202 while pending/processing or 200 with an existing completed/failed result. It never applies reconciliation twice. Changed content returns 409 snapshot_reference_conflict; switching to staged delivery returns 409 snapshot_delivery_mode_conflict.