Connecting your own billing system: merchant-provider/v1
This guide is for engineers at a merchant whose customers, charges and subscriptions live in its own system (a billing system, or several payment providers behind one). To use Chargeless you expose merchant-provider/v1: a small API that Chargeless calls to read facts and to execute the refunds and cancellations its policy approved. It comes in two bindings, REST and MCP, with the same operations and the same rules. The guide says what to build and how to prove it works before you go live. The REST binding by example and The MCP binding show each binding on the wire, each with a complete server you can start from.
The contract is a set of files Chargeless gives you. Where this guide and the files differ, the files win.
| File | What it holds |
|---|---|
openapi/merchant-provider/v1.yaml |
The REST binding (OpenAPI 3.1): paths, headers, status codes, the contract terms, the signature with test vectors, the outcome callback |
schemas/merchant-provider/v1/*.json |
One JSON Schema (draft 2020-12) per object. Each $id is https://schemas.supportroute.dev/merchant-provider/v1/<file> |
schemas/merchant-provider/examples/v1/ |
Valid examples of every object, and invalid.json with the cases Chargeless refuses |
mcp/merchant-provider/v1.json |
The MCP tool profile |
CHANGELOG.md |
Every change to the contract |
1. What Chargeless does with your system, and what it does not
- Chargeless decides. Every refund and cancellation is decided by the policy you publish in Chargeless: windows, amount caps, charge types, velocity limits, and rules over the facts your system reports. There is no eligibility operation, and your system never decides.
- Your system supplies facts and executes. Chargeless reads the customer, the charge or the
subscription from your system before every decision. It calls
refundorcancel_subscriptiononly for an action it has approved and recorded under an idempotency key. - A refusal at execution is an outcome, never a decision input. If your system refuses an approved action, that action stops. Depending on the refusal class (section 6), the customer gets a scripted denial or the case goes to your team. The recorded decision stays as it was, and a refusal can never approve anything. If you find yourself refusing a whole class of requests, write that rule into your Chargeless policy instead.
- Facts are fresh. A decision uses a target read from your system in the last 60 seconds. The refundable amount Chargeless decides on is the charge amount, minus the larger of the refunds you report and the refunds Chargeless executed, minus Chargeless's own refunds still in flight. Your own over-refund refusal (term 3 below) covers refunds made elsewhere in between.
- No person waits. Every automated case ends in one of three ways: executed, denied with a scripted reason, or handed to your team. When your system cannot be reached, a chat is handed off at once with a scripted message. A ticket is retried for up to 15 minutes and then handed off. A decision without a fresh read is handed off too.
- What Chargeless never does:
- a language model never calls your system and never sees your operations;
- it never calls a tool or an endpoint outside the contract;
- it never sends a refund destination, because refunds go to the original payment method;
- it never refunds a
disputedcharge automatically, and never attempts a refund on a charge whoserefund_routeisunsupported; - it runs no chargeback or early-warning workflow for your charges; the chargebacks you report are facts for your policy;
- it converts no currencies: a charge in a currency other than your policy's is handed to your team.
- Kill switches. Your workspace's kill switch, and Chargeless's, stop every automated refund and cancellation. No contract check runs while one is on.
2. The operations
| Operation | REST | MCP tool | Required | When Chargeless calls it |
|---|---|---|---|---|
describe |
GET /v1/describe |
sr_describe |
yes | At the contract check, and when your connection settings change |
find_customers |
POST /v1/customers/search |
sr_find_customers |
yes | To narrow candidates from identifiers. A match never proves who is writing |
get_customer |
GET /v1/customers/{customer_ref} |
sr_get_customer |
yes | For identity proofs, and with every decision |
list_transactions |
GET /v1/customers/{customer_ref}/transactions |
sr_list_transactions |
yes | Every time a verified customer's transactions are looked up |
get_transaction |
GET /v1/transactions/{transaction_ref} |
sr_get_transaction |
yes | Before every decision on that charge |
get_subscription |
GET /v1/subscriptions/{subscription_ref} |
sr_get_subscription |
with cancel_subscription |
Before every decision on that subscription |
refund |
POST /v1/refunds |
sr_refund |
yes | Only for an approved refund, with its key |
cancel_subscription |
POST /v1/subscriptions/{subscription_ref}/cancel |
sr_cancel_subscription |
with cancel_subscription |
Only for an approved cancellation, with its key |
get_operation |
GET /v1/operations/{idempotency_key} |
sr_get_operation |
yes | To confirm an outcome that was left open, and after an outcome callback |
list_payment_attempts |
POST /v1/payment-attempts/list |
sr_list_payment_attempts |
with payment_attempts |
After go-live, to fill payment insights (revision 1.1; never a decision) |
Paths follow your base URL, which may include a prefix (https://billing.example.com/supportroute
→ /supportroute/v1/refunds). Reads sit on live chat and ticket paths, so make them fast: the
contract check fails a read slower than 2.5 s (section 10).
The objects
Every field listed here must be present in your answers. "Or null" means the value may be null,
but the key must still be there. The schema files hold the exact patterns.
describe:contract_version:"1.0";"1.1"once you implementlist_payment_attempts;"1.2"once you send the optional subscription fields of revision 1.2. Earlier revisions stay valid unchanged;capabilities: the four booleans from 1.0, and one optional flag from 1.1:partial_refunds: without it Chargeless only ever asks for the full remainder;cancel_subscription;cancel_at_period_end: needscancel_subscription;outcome_callbacks;payment_attempts(1.1, optional; absent means false): you implementlist_payment_attempts. Without it Chargeless never calls that operation, and the panel says your system does not send payment attempts;
currencies: 1 to 200 ISO codes you charge and refund in;refunds_to_original_payment_method:true.
find_customerstakes one or more ofemail,phone(E.164),customer_ref,transaction_ref,order_refandcard_last4. It returns the customers that fit every identifier given, at most 10, as{"candidates": [{"customer_ref": …}]}. Match an email case-insensitively against every address on file. No match is200with an empty list, never a- Chargeless compares every identifier again itself, on fresh data.
Customer:
customer_ref;emails: at most 10 addresses the customer controls, primary first;phones: at most 10, E.164;account_status:active,closedorrestricted;account_created_at;prior_refunds: the last 365 days, from every charge and whoever made them, newest first, at most 50;prior_chargebacks: the last 365 days, at most 50, each with a status ofwarning,open,wonorlost;- optional
attributes.
The email code and the email link that prove a customer's identity go to an address on file. Contact points are used only for those proofs: they are never shown to a model, to the customer or in the panel. A customer whose proven address is no longer on file loses that proof at the next decision.
Transaction:
transaction_refandcustomer_ref, the current owner: ownership is decided on it, read fresh;order_refor null;subscription_refor null;state:pending,succeededorfailed; this describes the charge itself, not refunds;amount;created_at: the policy's refund window counts from here;descriptionor null: at most 200 characters, shown to staff only;product_typeor null, a label;charge_type:initial,renewal,trial_conversion,upgrade,one_timeortop_up;processor: a label such asstripeorin_house;refund_route:providerorunsupported;card_last4or null;card_brandor null;disputed;refunds: every refund that took effect, oldest first, at most 100. Each hasat,amount,idempotency_key(Chargeless's key, byte for byte, ornullfor a refund made any other way) andreversed_at(or null). A pending refund is not listed until it takes effect. A reversed refund stays listed;settlementor null: the charge in your settlement currency. It is informational, and Chargeless never converts with it;- optional
attributes.
Subscription:
subscription_ref;customer_ref;phase:trial,active,grace,past_due,paused,canceledorexpired.canceledandexpiredare final;cancel_at_period_end;current_period_endor null;trial_endor null;billing_interval, as{unit, count};plan_refor null;renewal_count;created_at;last_chargeor null, as{transaction_ref, at, amount};- optional
attributes; - revision 1.2, optional (absent means null):
canceled_at,ended_at,price(perbilling_interval) andplan_name(at most 120 characters). They are shown in the merchant panel and kept for analytics, never used in a decision. Optional does not mean lenient: an invalid value (aplan_nameover 120 characters, a malformedcanceled_atorended_at) fails the whole read, as any other field does.
Refund request:
transaction_ref,amount(at least one minor unit, in the charge's currency),reasonanddry_run.Refund result:
idempotency_key: as received,nullonly for a dry run;status:succeeded,pendingorwould_succeed;transaction_refandamount: as requested;refund_refor null;processed_at: required withsucceeded.
Cancellation request:
at_period_end,reasonanddry_run.Cancellation result:
idempotency_key;status;subscription_refandat_period_end: as requested;effective_at: required withsucceeded, null for a dry run or while pending.
Operation (
get_operation):idempotency_keyandstatus(unknown,pending,succeededorfailed). The other fields depend on the status:Status Also carries unknownnothing else pendingorsucceededoperation,target_ref,result,updated_atfailedby a refusaloperation,target_ref,refusal: {code, message},updated_atfailedby a reversaloperation: refund,target_ref,result,reversed_at,updated_atattributes: at most 20 keys matching[a-z][a-z0-9_]{0,39}. Each value is a string of at most 200 characters, an integer or a boolean. They are readable by your policy's rules and never shown to a model or a customer. Put no contact or card data in them.reason: one ofduplicate,forgot_to_cancel,unrecognized,not_as_described,service_issueorother. It is for your books only and never changes what the operation does. The set is open, so treat an unknown value asother.
Unknown objects are 404 on get_customer, list_transactions, get_transaction and
get_subscription. Each answer must be about the object asked for, and every item of
list_transactions must belong to the customer asked for. list_payment_attempts is not one of
those reads: without the capability answer 404; with it, an empty page is 200.
Payment attempts (optional, revision 1.1)
Declare payment_attempts only when you can list every card attempt your processor reports,
succeeded, failed and still pending. Chargeless uses them for the payment insights page
(breakdowns by country, brand, funding, issuer and BIN, and monitors that only notify). They
never feed a refund or a cancellation, and a model never sees them.
- The query is the signed body of
POST /v1/payment-attempts/list(no query parameters):updated_since(RFC 3339, required, inclusive),cursor(optional, the previous page'snext_cursor) andlimit(1–200, default 100). - Order. Return attempts whose
updated_atis at or afterupdated_since, ascending by (updated_at,attempt_ref), at mostlimit.next_cursoris present exactly whenhas_moreis true, and a page that claims more has at least one attempt. The cursor is opaque, 1–512 characters of the contract's cursor alphabet, and must stay valid for at least 24 hours for the sameupdated_since. An unknown or expired cursor is400 invalid_request: Chargeless then reads again from its bound without a cursor. A400on a first page (no cursor) is a refused request, not an expired cursor. - One attempt, one
attempted_at.attempt_refis stable for the attempt's life.attempted_atnever changes: a later report with anotherattempted_atis a contract violation. When anything else changes (a pending attempt that settles, a code added), set a laterupdated_at, at or afterattempted_at, and make the change visible to this read within 60 seconds.succeededandfailedshould then stay final. - What each attempt carries. Every key is present; a value you do not know is
null.statusissucceeded,failedorpending.amountis the amount attempted, in the currency attempted.cardisnullfor a non-card attempt; otherwise it carriesbrand,network,funding(credit,debit,prepaidorunknown),country,bin(6–8 digits, never more),pan_length(12–19, ornull),last4,issuer(at most 200 characters, no control characters),walletandthree_ds_result.failure_categoryisnullexcept on a failed attempt, where it may benulland Chargeless derives it from the decline codes. Decline, network-decline and advice codes are at most 64 characters.risk.scoreis 0–99 ornull.billing_countryandip_countryare the only location facts. - Never a card number. No PAN, expiry, security code, cardholder name, email, postal address
or IP address, in any field. A reference, a code or a cursor that contains a run of 13 or more
digits (separators of up to three characters between them still count) is refused as
card_number_in_field. Chargeless storesbintruncated: 8 digits only whenpan_lengthis 16 or more, otherwise 6, and card-masksissuerbefore storing it. - Pace. Chargeless reads pages one after another, never in parallel: the 13 months after
go-live, then at most 10 pages per workspace per minute (
X-SR-Channel: system, one correlation id per sync run). Answer429withRetry-Afterto slow it. The correlation id here is the sync run's id, not an action or a ticket. - Without the capability the operation is never called. A 1.0
describestays valid.
Money
amount_minoris a JSON integer in the currency's ISO 4217 minor unit: JPY 0, KWD 3, most others 2. Use the ISO exponent, never your own convention.1999is valid.1999.0,19.99,1.999e3,"1999"and negative amounts are refused, even when the value is whole.currencyis an upper-case active ISO 4217 code with a defined minor unit. Precious metals, funds, testing codes and withdrawn codes (for example BGN since 2026-01-01) are refused.- Chargeless compares currencies before amounts. A result must echo the request's amount and currency exactly.
Sizes and time limits
| What | Limit |
|---|---|
| Our timeouts | 5 s for reads, describe and get_operation; 20 s for mutations |
| Response body | 1 MiB |
| Reference | 1 to 128 characters (section 7) |
find_customers candidates |
10 |
list_transactions |
50 transactions and 50 subscriptions |
refunds[] on a transaction |
100 |
prior_refunds, prior_chargebacks |
50 each, last 365 days |
A customer's emails, phones |
10 each |
attributes on a customer, transaction or subscription |
20 entries |
describe currencies |
200 |
list_payment_attempts page |
200 attempts |
description |
200 characters |
Error message |
500 characters |
| A number | 32 characters, exponent within ±400 (1e400) |
| Nesting | 64 levels of objects and arrays |
| Any other array, any object (fields you add included) | 256 items or members |
Chargeless validates every answer against the published schemas, and checks every value again
in code. It ignores fields it does not know, so an answer from a later 1.x revision still works.
Every property must appear once and be spelled exactly as in the schema: a repeated key, or one
that differs only by case (Status, CARD_LAST4), makes the whole answer invalid.
Before it validates, Chargeless holds the answer to the size limits above, checked as it reads.
They also apply to fields it does not know: an extra array of 257 items, an extra object of 257
fields, or nesting 65 levels deep anywhere in the answer makes the whole answer invalid, and so
does a number of 33 characters or 1e401, even where the schema would accept it. Such an answer
is a contract violation (schema_invalid; over MCP, a number too large for a float64 is refused
earlier, as mcp_protocol). It fails closed, as any invalid answer does: a read fails, and a
refund or cancellation is treated as an unknown outcome until get_operation confirms it
(section 5). On a call that serves a customer, it also opens a Contract violation alert (section
3). Every value the schemas allow is within these limits: an amount is an integer of at most 20
characters, and Go, JavaScript and Python print any float64 in 24 characters or fewer.
The array and map limits in the table apply where the schemas declare them. Every other array and object has the general limit of 256, including the fields of a declared object, where later 1.x revisions add fields.
3. Authentication and signing
The signature (always)
Every request carries X-SupportRoute-Timestamp (Unix seconds) and X-SupportRoute-Signature: v1=<hex>. The hex value is the lower-case HMAC-SHA256, keyed with your signing secret, of
six fields joined by a line feed (\n):
{timestamp}\n{METHOD}\n{host}\n{path}\n{idempotency_key}\n{body}| Field | Value |
|---|---|
timestamp |
The X-SupportRoute-Timestamp header, as sent |
METHOD |
Upper case: GET or POST |
host |
The authority as sent, lower case, with :port only when the port is not the default. A production call signs billing.example.com. Behind a proxy that rewrites Host, verify against your public host name |
path |
The path exactly as sent: base path included, percent-encoding as sent, no query |
idempotency_key |
The Idempotency-Key header as sent, or the empty string when there is none (reads, get_operation, dry runs). The empty string leaves an empty line |
body |
The raw request bytes, empty for a request without a body |
To verify a request:
- compute the HMAC over the bytes you received, before parsing anything;
- compare in constant time;
- refuse a missing or wrong signature, and a timestamp more than 5 minutes from your clock, with
401.
The contract check sends a describe signed with a wrong key and one signed correctly 10 minutes ago. Both must be refused (section 10).
import hmac, hashlib, time
def verify(secret, ts, method, host, path, idem_key, body, header):
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return False
msg = b"\n".join([ts.encode(), method.upper().encode(), host.lower().encode(),
path.encode(), idem_key.encode(), body])
want = "v1=" + hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, header)Test vector (more are in the OpenAPI file under x-supportroute-test-vectors):
secret srmp_test_4f7c1e9a2b6d
timestamp 1760520000
method POST
host billing.example.com
path /v1/refunds
key 0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6
body {"transaction_ref":"ch_1001","amount":{"amount_minor":2000,"currency":"USD"},"reason":"service_issue","dry_run":false}
signature v1=1b03ea29388ff9f1dd2bbf894f1e6e359f0fa1003ca246c678ad485249503070The host is signed so that a request sent to one endpoint (your sandbox, or a new URL being checked) never verifies at another. The idempotency key is signed so that no intermediary can swap it.
Your three secrets
All three are generated by Chargeless and shown once in Settings → Payment provider. Saving them needs an owner, admin or developer of your workspace with an authenticator code.
| Prefix | Name | Used for |
|---|---|---|
srmp_ |
Signing secret | Chargeless signs every production call with it. Generated when you first save the connection. A new one is staged, deployed by you and then activated (below) |
srmps_ |
Sandbox signing secret | Only the conformance kit signs with it (section 10). Configure it on your sandbox only: your production endpoint must refuse it. Issued on the first save, and again on request. Chargeless does not store it |
srmpc_ |
Callback secret | You sign outcome callbacks with it (section 8). Generated the first time you turn callbacks on, and on rotation. After a rotation, the previous one keeps working for 7 days or until you retire it |
Rotating a secret
The signing secret rotates in three steps. Chargeless keeps signing every call with the current secret until the last step.
Stage. Settings → Payment provider → Signing secret → "Stage a new secret". The new
srmp_secret is shown once. Nothing changes for your endpoint yet.Deploy. Let your verifier accept both secrets, and deploy the new one.
Activate. "Activate" runs the contract check (section 10), with every call signed with the new secret.
- If the check passes, Chargeless signs with the new secret from then on.
- If your endpoint refuses the new secret, nothing changes, and the report names the failed
lines. An undeployed secret fails
describe.
Keep accepting the old secret for at least 10 minutes after activating, so that calls already under way can finish with it. Then remove it.
- A staged secret does not expire. Staging again replaces it. "Discard" removes it; Chargeless never signed with it.
- An activation is a contract check. It does not run while a kill switch is on, and it counts toward the 10 checks per 10 minutes. A kill switch that comes on during the check refuses the activation.
- An activation changes your connection's configuration version. An action that Chargeless
sent before the activation is not sent again on an
unknownanswer; polling still confirms it (section 5). - "Rotate now" is for a leaked secret. The new secret applies at once, without a check, and it
discards a staged secret. Until you deploy it, your endpoint refuses Chargeless's calls with
401, and the cases those calls serve go to your team.
The callback secret rotates with an overlap. "Rotate the callback secret" shows the new
srmpc_ secret once. The previous secret keeps verifying your callbacks for 7 days, or until you
retire it ("Retire"); deploy the new one within that time.
- "Rotate now (retire the old one)" retires the previous secret in the same step, for a leaked secret.
- At most two callback secrets verify at a time, so a new rotation retires a previous one that still verifies.
- Only a callback signed with the current secret counts as the verified test event of section 10.
Who can change a secret. Every secret change needs an owner, admin or developer with an authenticator code. Discarding a staged signing secret and retiring a previous callback secret only take access away, so both also work while Chargeless has suspended your workspace.
Your own credentials (the bearer token and the edge headers, below) take effect when you save them. Accept the new value on your side first, then save it.
Optional credentials
- Bearer token. A token you issue to Chargeless, sent as
Authorization: Bearer <token>on every call. It never replaces the signature. - Edge headers. Up to 3 headers that your own zero-trust edge or tunnel requires, sent on every
call.
- Names: 1 to 64 characters of
[A-Za-z0-9-]. These names are reserved:Authorization,Host,Accept,User-Agent,Idempotency-Key,Content-*,X-SupportRoute-*,X-SR-*, and over MCP alsoMcp-*andLast-Event-ID. - Values: up to 1,024 printable ASCII characters.
- Names: 1 to 64 characters of
- Credentials stay with their host. When you move the base URL to another scheme, host or port, set or clear the bearer token and the edge headers in the same save. A stored credential is never sent to a new host.
Network
- Your endpoint.
- The base URL is
https://host[/base path]on port 443, with a public host name and a certificate from a publicly trusted CA. TLS 1.2 or later. - No query, fragment or user info in the URL.
- Calls never follow redirects: answer a
3xxand the call fails.
- The base URL is
- Egress IPs.
- Calls come from Chargeless's published IPv4 addresses: one in the test environment and three in production, each environment with its own list. Allowlist every address of the environment you connect to.
- Copy the list from the Egress section of Settings → Payment provider, which also shows an announced address before it is used.
- Changes are announced at least 30 days ahead, in the panel and by email to your owners and admins.
- The list only narrows who can reach you: always verify the signature as well.
- mTLS (optional).
- Turn on "Client certificate (mTLS)" and Chargeless presents its per-environment client
certificate, subject
O=Chargeless, CN=supportroute-<env>-egress. - Pin the CA shown under Egress, never the client certificate itself. The client certificate is rotated yearly without notice; a CA change is announced at least 90 days ahead.
- The certificate says the call comes from Chargeless. The signature says which Chargeless workspace is calling, so verify it with or without mTLS.
- Turn on "Client certificate (mTLS)" and Chargeless presents its per-environment client
certificate, subject
Correlation
Every call carries these headers. Log them, because the panel's provider call log (Settings → Payment provider, kept 90 days) shows the same ids.
| Header | Value |
|---|---|
X-SR-Request-Id |
A UUID, new for every attempt |
X-SR-Correlation-Id |
The action id for mutations; otherwise the case, ticket or chat session the call serves. For list_payment_attempts, the id of that sync run |
X-SR-Channel |
chat, ticket, helpdesk, staff or system. Treat an unknown value as system |
X-SR-External-Ticket |
The helpdesk's ticket id, when a helpdesk ticket is the origin |
The User-Agent is Chargeless (merchant-provider/v1). It is not a credential.
Provider alerts
Chargeless watches its own calls to your system. Every minute it evaluates the last 15 minutes of calls that serve customers: calls for an action, a case, a ticket or a chat. It opens an alert when:
| Alert | Opens when | Resolves when |
|---|---|---|
| Credentials rejected | One call is refused with 401 or 403. Payment attempt syncs count too |
A later call succeeds, or a contract check passes after the last refusal. Otherwise after 24 hours without a refusal |
| Contract violation | One answer breaks the contract: it does not validate (section 2), or it is 409 idempotency_conflict |
After 24 hours without another |
| Provider unavailable | At least 5 calls fail with 5xx, 429, a timeout, a connection error or a redirect, and they are at least a quarter of the window's calls |
Two evaluations in a row below an eighth, with at least one call that went through. Otherwise after 24 hours without a failure |
- What never counts. The contract check's own calls never open an alert: their report goes to
the person who ran the check. Chargeless's own timeouts never count either. Such a call shows
in the call log as
timeoutwith detailvalidation_busy, and as "Busy (Chargeless)" in the tallies: Chargeless could not read your answer in time. A read then fails as a timeout would, and a refund or cancellation's outcome is confirmed by its key (section 5). - Who hears it.
- Email goes to your workspace's active, verified owners, admins and developers. They get an email when an alert opens, and alerts that open together share one email. Each alert is emailed at most once, and an alert of the same kind is not emailed again within 6 hours.
- No email goes out while Chargeless has suspended your workspace. An alert that is still open when the workspace is active again is emailed then.
- The panel shows open alerts, the alerts resolved in the last 7 days and the last 24 hours of calls by outcome in Settings → Payment provider → Provider health. Settings shows a badge, and Home shows a banner.
- Each alert names up to 3 request ids (
X-SR-Request-Id) of the calls that triggered it, to search your logs for. - Provider health also counts error answers whose
retryableflag disagrees with their status (section 5). They are deviations, never alerts.
- Alerts only notify. Chargeless never pauses your connection, retries differently, moves a handoff or decides anything because of an alert. A refused or broken call ends exactly as section 5 says.
- Monitoring. A monitoring system can poll the same state with one of your workspace's API
keys:
sr.cases.v1.CasesService/GetProviderHealth. The answer may be up to 30 seconds old and is shared by every caller of your workspace, so polling more often than that returns the same answer. Aresource_exhaustedanswer asks you to retry after a short pause.
4. Idempotency
- The key. Every
refundandcancel_subscriptionthat is not a dry run carriesIdempotency-Key: {workspace_id}:{action key}, at most 255 characters of[A-Za-z0-9:_-], for example0199a3c1-…-101112131415:ticket:0199e2c4-…:refund:0199e2c4-…. The prefix keeps keys unique when one endpoint serves several Chargeless workspaces.- Treat the key as opaque: store and compare it whole, byte for byte.
- A mutation without a key that is not a dry run is
400 invalid_request.
- Same key, same parameters. Return the original result and never execute twice. Returning
the operation's current state is fine, for example
succeededfor a refund that waspendingat the first call. - Same key, different parameters. Answer
409with codeidempotency_conflictand execute nothing. Chargeless treats this as final and records it as a security event. - Same key while the first call still runs. Answer
409with codein_progress. Chargeless retries with the same key. - Per key, not per charge. A deliberate second partial refund on the same charge comes with another key.
- Traceable. A refund made through the contract shows its key in the charge's
refunds[]and in the customer'sprior_refunds, exactly as received. That is how Chargeless counts its own refunds once. - Keep keys for at least 30 days.
get_operationmust answer for every key you received in that time. - Dry runs (
"dry_run": true) carry no key. They validate fully, answerwould_succeedor a refusal, change nothing and store nothing. Only the contract check and the conformance kit send them, never the decision path. - Chargeless's retries. Within one call, Chargeless makes up to 3 attempts with the same
key and a new
X-SR-Request-Id. It retries after a connection error,429,5xxor409 in_progress, backing off from 200 ms to 2 s. It honoursRetry-After(in seconds) up to 2 s; a longer wait is left to a later retry. A timed-out mutation is never retried within the call: its outcome is unknown, and Chargeless confirms it by key (section 5).
5. Outcomes and finality
How Chargeless reads your answer
| You answer | On a read | On a mutation |
|---|---|---|
2xx with a valid body (201 and 202 are read as 200) |
The value | succeeded or pending |
2xx whose body is invalid, or does not echo the key, target or amount |
Failure (a contract violation) | Unknown outcome, confirmed by get_operation |
400 invalid_request |
Final failure | Final: nothing retried, the case goes to your team |
401 / 403 credentials_rejected |
Final failure | Final, as above |
404 not_found |
Not found | A guard-class refusal (section 6) |
409 idempotency_conflict |
— | Final, nothing executed |
409 in_progress |
— | Retried, then unknown |
422 with a refusal code |
— | A refusal (section 6) |
429, 5xx, timeout, connection error |
Retried, then a failure | Retried, then unknown |
3xx, any other status, an unreadable error body |
Failure | Unknown |
Errors use the envelope {"error": {"code", "message", "retryable"}}. retryable must agree with
the status, and on a 422 it is always false:
falsefor400,401,403,404,409 idempotency_conflictand422;truefor409 in_progress,429and5xx.
Chargeless acts on the status only: a flag that disagrees never changes an outcome, a retry or a
refusal class. It records the flag on every error answer, and the call log shows a disagreement.
The contract check and the kit go further on the errors they provoke on purpose (section 10): a
flag that disagrees with the status, or a missing flag, fails that line with retryable_mismatch.
On live traffic a missing flag is only recorded, and the call log shows it as a dash.
Once an attempt may have reached you, the outcome stays open. Only a valid 2xx or a readable
refusal on a later attempt settles it. A 400, 401 or timeout on a retry says nothing about what
the earlier attempt did, so Chargeless asks get_operation instead. Never answer 401, or
anything else, to a key you have already executed: replay its result.
The finality terms you agree to
succeededandfailedare final for a key. A refusal (422) orfailedmeans that no money moved under that key, and none ever will.- The one later change allowed is a reversal: you returned a succeeded refund. Report it from
get_operationasfailedwithreversed_atand the refund'sresult. Keep the refund in the charge'srefunds[]withreversed_atset. Chargeless then stops presenting the refund as done and tells your team. The refund keeps counting against the charge, so Chargeless does not refund that amount again by itself. unknownis only for a key you never received.get_operationnever answers404.
A key that answered failed and later succeeded breaks term 1. Chargeless ignores the success
and records a contract violation, while its refundable amount and velocity limits keep counting
the refund you report. Changing a succeeded key to anything but a reversal, or answering
unknown after any answer, is a violation too.
Polling
A mutation left open (pending, timed out, or answered unreadably) is confirmed with
get_operation on this schedule after the call:
- 30 s, 2 min, 10 min and 30 min;
- then every hour, up to 24 hours.
A final answer ends the polling. An outcome callback (section 8) only brings the next read forward.
unknownwithin those 24 hours. Chargeless sends the mutation again with the same key, which is safe under term 1. It does not re-send if a kill switch was on, or your connection settings changed, since the first attempt; the case then goes to your team.- After 24 hours. The action goes to your team as unconfirmed. A refund still counts as in flight, so it is never refunded twice. Chargeless then reads the key once a day until day 30, applies a late answer and never sends anything again.
6. Refusals
A refusal is a 422 from refund or cancel_subscription:
{"error": {"code": "merchant_hold", "message": "Finance hold on this account", "retryable": false}}A failed operation reports the same refusal in get_operation. Every code has a class.
| Class | Codes | What happens |
|---|---|---|
| State: your records contradict the facts Chargeless decided on | already_refunded, amount_exceeds_refundable, not_refundable, subscription_not_active, already_canceled, currency_mismatch |
The action ends denied and the decision stays recorded. Chargeless reads the target again, and the customer gets the scripted denial |
| Guard: your own execution guard | account_restricted, merchant_hold, any custom x_ code, any code not listed, and a 404 on the target |
The action ends failed and the case goes to your team (reason provider_refused), with the code and your message |
- Custom codes are
x_followed by 1 to 62 characters of[a-z0-9_]. They are always guard class. - Messages are at most 500 characters of plain text. They are shown to your staff only, card masked, and never to a model or a customer. Put no card data, contact data or secrets in them.
- An unreadable
422(no envelope, a repeated key) is not trusted as a refusal: the outcome is unknown andget_operationsettles it. - Over-refunds. Refuse a refund above your own refundable amount with
amount_exceeds_refundable. The contract check and the conformance kit both test it.
7. References
Customer, transaction, subscription, order, plan, refund and event ids follow one rule:
- 1 to 128 characters of letters, digits,
.,_,:and-, starting with a letter or a digit. They are stable for the object's life and never an email address, a phone number or a card number. - No run of 13 or more digits, whether adjacent or joined by up to three of
.,_,:and-, and whether or not letters surround the run. Card numbers have 13 to 19 digits, so such a reference is refused as a card number, whatever its check digit, and is never stored.- Refused:
4528730562594,4242-4242-4242-4242,ord_2026101500012,112-3456789-1234567. - Accepted:
ch_1001,gp.4031-2290,ord-2026-10-15-0001(12 digits).
- Refused:
- The exception: canonical UUIDs. A UUID in 8-4-4-4-12 form, all lower or all upper case,
version 1 to 8, variant
8,9,aorb, is accepted even when its digits form such a run. - Numeric ids. Encode them with an alphabet that holds few digits, such as base32 (RFC 4648) or base62. A prefix does not help, and neither does other hexadecimal: about one SHA-256 hex digest in 23 holds such a run.
The contract check tests your fixture references and every reference your reads list, and the conformance kit does the same, so a reference problem shows up before go-live rather than on a customer's ticket.
8. Outcome callbacks (optional)
Polling is the guarantee. A callback only tells Chargeless to read an outcome sooner.
Declare it. Declare
"outcome_callbacks": trueindescribe.Turn it on. Turn on Outcome callbacks in Settings → Payment provider. You receive the callback secret (
srmpc_…, shown once) and the callback URL,https://<Chargeless's webhook host>/webhooks/merchant/{workspace_id}.Send events. When the operation under one of Chargeless's keys changes (it settled, it was refused, or a succeeded refund was reversed), POST:
{"event_id": "evt_01JAB7Q3W2XKZ8M4N6P0R5S9TV", "type": "operation.updated", "idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6", "occurred_at": "2026-10-17T09:41:12Z"}idempotency_keyis the key exactly as Chargeless sent it.event_idis a reference by section 7's rule, unique per event and reused byte for byte on every retry of that event.- The body carries no status or amount, because none is ever applied.
Sign it. Sign the request with the callback secret, in the same scheme as section 3:
{timestamp}\nPOST\n{Chargeless's host}\n/webhooks/merchant/{workspace_id}\n\n{body}- The key field is empty.
- The headers are
X-SupportRoute-TimestampandX-SupportRoute-Signature. - The OpenAPI file has two test vectors.
Chargeless then checks the request, in order:
- the body is at most 16 KiB;
- the signature verifies, with a 5-minute timestamp tolerance;
- the body is valid against the schema;
- the
event_idis new (repeats are dropped for at least 30 days).
Only then does it read get_operation for the key and apply that answer. A key Chargeless does
not hold, or one for another workspace, is accepted and ignored.
| Answer | Meaning | What to do |
|---|---|---|
204 |
Accepted, a repeat, or a key we do not hold | Nothing |
400 |
The body is not a valid operation event | Fix the sender; do not retry |
401 |
A wrong or missing signature, a timestamp out of tolerance, callbacks off, or an unknown workspace. The panel shows the last refusal's reason | Sign again with the current secret before retrying. During a rotation's 7-day overlap, the previous secret still verifies (section 3) |
413 |
The body is over 16 KiB | Do not retry |
429, 5xx |
Rate limited, or not recorded | Retry the same body and event_id, signed again, with exponential backoff for up to 24 hours. Honour Retry-After |
Limits:
- Callbacks for keys older than 30 days are ignored.
- At most 20 callback reads are made per action. Beyond that, polling alone confirms the outcome.
- Chargeless accepts up to 600 callbacks a minute from one address, and 120 a minute from one address for one workspace.
- Once an address has delivered a verified callback, the workspace's verified senders share 240 a minute.
- Never hold anything back waiting for a callback's answer: a lost callback costs nothing but time.
The callback works the same with the REST and the MCP binding.
9. The MCP binding
Choose the MCP transport in Settings → Payment provider when your system offers MCP tools rather than REST. Everything in sections 1 to 8 applies; only the wire differs:
- Transport. You run an MCP server over Streamable HTTP at your base URL, with no
/v1/…paths, and Chargeless is the client. Each attempt opens its own session, makes onetools/calland closes it. - Signing. Every HTTP request of the session is signed as in section 3, with an empty key
field. The idempotency key travels in the tool's
idempotency_keyargument, inside the signed body; there is noIdempotency-Keyheader. - Correlation. The ids of section 3 travel in
params._meta(dev.supportroute/request_id,dev.supportroute/correlation_id,dev.supportroute/channel,dev.supportroute/external_ticket). - Results. The object goes in
structuredContentand is validated exactly like a REST body. An error is a result withisError: truewhosestructuredContentis the error envelope; its code maps to a REST status. A JSON-RPC error counts as a5xx. - Schemas. Every profile tool declares its
inputSchemaandoutputSchemaas a$refto the contract's$id. Tools outside the profile are ignored and never called.
The MCP binding has the whole binding: the tools with their arguments, a session on the wire, the error mapping, the schema table and limits, and a complete server in Go that passes the conformance kit. The REST binding by example shows every REST operation on the wire, with a complete server in Node.
10. Proving it
Go-live takes two steps, and neither moves money on live data.
Step 1: the conformance kit against your sandbox
The kit, providercheck, proves what a live system cannot show without moving money. It runs the
live check's lines first, then real sandbox mutations. Download it for macOS, Linux or Windows
from providercheck, which also shows how to check the file and read
its output. Chargeless can also run it with you, using your sandbox credentials.
Configure the sandbox. Put the sandbox signing secret (
srmps_…) on your sandbox, and only there.Pick fresh fixtures. Choose a sandbox customer, a refundable charge of that customer, and, with
cancel_subscription, an active subscription of that customer. The run refunds the charge (one minor unit withpartial_refunds, otherwise the whole remainder) and cancels the subscription, so use fresh fixtures for every run.Run the kit. Secrets come only from the environment.
export PROVIDERCHECK_SANDBOX_SIGNING_SECRET=srmps_… # optional: PROVIDERCHECK_BEARER_TOKEN=… PROVIDERCHECK_EDGE_HEADERS='[{"name":"…","value":"…"}]' (at most 3) # REST providercheck -sandbox \ -base-url https://sandbox.billing.example.com/supportroute \ -tenant <your workspace id> \ -customer cus_… -transaction ch_… -subscription sub_… \ -out providercheck-report.json # MCP: the same, with your sandbox's MCP endpoint as -base-url providercheck -sandbox -transport mcp \ -base-url https://sandbox.billing.example.com/mcp \ -tenant <your workspace id> \ -customer cus_… -transaction ch_… -subscription sub_… \ -out providercheck-report.jsonRead the result.
- Exit status
0means every check passed,1that a check failed, and2that the run could not start. - It prints one line per check, with the
X-SR-Request-Idof a failing call. - It writes a report sealed with a key derived from the sandbox secret.
- Exit status
Upload the report. Upload the sealed report in Settings → Payment provider → Conformance kit: choose the file, tick "This file is the kit's unmodified output for this workspace", then "Attest the report" (with any step-up). Going live needs a passed report for your transport from the last 30 days, and so does a later change of transport. A base URL or mTLS change does not.
What Chargeless records. The file must be at most 64 KiB, byte for byte as the kit wrote it. Chargeless records its SHA-256, the kit version, the run's id and time, the transport, the contract version, the sandbox's origin, each line's name, result and code, and the member who attested. Compare the SHA-256 shown with
shasum -a 256 providercheck-report.json. The file itself is not stored, so keep it.An attestation is a statement. Chargeless cannot verify the seal: its key derives from your sandbox secret, which Chargeless does not keep. The attestation is your member's statement that the file is the kit's output.
Without
cancel_subscription, the subscription checks (thecancel_*lines andone_effect_per_call) may be skipped. With it, they must pass. Chargeless does not yet ask for a new report when a live system starts declaringcancel_subscription. Run the kit with a subscription fixture and upload that report anyway.What is refused. An upload is refused, with its reason, when:
- the statement is not ticked;
- the file is unreadable or has no seal;
- the kit is older than
providercheck/2; - the report is for another workspace, a transport other than
restormcp, or a contract outside 1.x; - the run is more than 30 days old, or more than 5 minutes in the future;
- a line failed or a sandbox check is missing;
- no connection is saved yet.
A workspace can upload at most 10 files per 10 minutes.
Other flags:
| Flag | Use |
|---|---|
-client-cert, -client-key |
Your own test client certificate, for a sandbox that requires mTLS |
-allow-private |
A sandbox over plain http, on another port or at a private address |
-settle-timeout |
How long to wait for a pending outcome: default 2 min, at most 30 min |
-v |
Log every call |
The kit has three safeguards:
- it signs only with an
srmps_secret and refuses ansrmp_one; - it needs
-sandbox; - it refuses to run next to Chargeless's own configuration.
The report names your sandbox by its origin, so the kit refuses an origin or a fixture the report could not hold, before it sends anything:
- The origin must be
http(s)://host[:port], with a DNS name or a bracketed IPv6 address and no run of 13 or more digits. An IPv4 address whose digits and port join into such a run, such as192.168.100.200:12112, is refused: use a host name instead. - The fixture references must follow section 7.
The sandbox checks, after the live lines:
| Check | Passes when |
|---|---|
refund_over_refundable |
A real refund one minor unit over the remainder is refused with amount_exceeds_refundable |
refused_key_final |
That key replays refused, get_operation says failed, and no money moved |
refund_executes |
The refund executes once and refunds[] lists it under Chargeless's key |
refund_replay |
The replay returns the original result and refunds nothing more |
refund_conflict |
The key with another amount gets 409 idempotency_conflict, and nothing runs |
cancel_executes, cancel_replay, cancel_conflict |
The same for an immediate cancellation |
cancel_refused_key |
A second cancellation is refused with already_canceled, replays refused and reads failed |
one_effect_per_call |
The refund leaves the subscription alone, and the cancellation leaves the charge alone |
operation_states |
Every key read again keeps its final state, and a key never sent is unknown |
A provoked error whose retryable flag disagrees with its status, or has no flag, fails its line
with retryable_mismatch (section 5). The provoked errors are:
- the over-refund and its replay (
refund_over_refundable,refused_key_final); - the conflicts (
refund_conflict,cancel_conflict); - the refused cancellation (
cancel_refused_key).
The kit does not test outcome callbacks. Over MCP, the sandbox checks run only after the tool profile check passes.
Step 2: the live contract check
The live check runs in Settings → Payment provider → Contract check → "Run the check", against your stored configuration, from Chargeless's egress. Go-live runs it again and goes live only if it passes for the configuration stored at that moment.
What it does. It reads and sends dry runs only. Nothing is refunded or cancelled.
Fixtures. Designate fixtures in your live system ("Test objects"): a quiet customer, a charge and, with
cancel_subscription, a subscription. A pending refund on the fixture charge is invisible to the check and can fail the remainder dry run, so pick a charge without one.When it runs. No check runs while a kill switch is on. One check runs at a time, and at most 10 run per 10 minutes.
Latency. A correct answer slower than 2.5 s for a read, or 10 s for a dry run, fails its line with
latency_budget_exceeded. Each call is measured once, so a cold start can fail a line: run the check again.Dry run order. The dry runs are sent least harmful first, and the fixture is read back after each one:
- a cancellation at period end (with
cancel_at_period_end); - a refund one minor unit over the remainder, which must be refused;
- one minor unit (with
partial_refunds); - the remainder;
- an immediate cancellation.
The check stops at the first change it sees (
dry_run_changed_state). Make sure your dry runs never execute.- a cancellation at period end (with
When to run it again. On a live workspace, a change of transport, base URL or mTLS is checked before it is saved, and a change of transport also needs an uploaded kit report for the new transport (step 1). Activating a staged signing secret runs the check too (section 3). After you change
describe(a new capability), run the check again: Chargeless uses the capabilities recorded by the last passing check.
| Line | Passes when | Typical failure codes |
|---|---|---|
reachability |
TLS and an HTTP answer that is not a redirect | timeout, redirect_refused, tls_handshake_failed, dns_failed, connection_failed, private_address |
describe |
A signed describe with version 1.x, the attestation and known currencies; then a wrong signature and a 10-minute-old timestamp are both refused with 401 or 403 |
signature_not_verified, stale_timestamp_accepted, auth_probe_unexpected, schema_invalid |
find_customers |
Searching by the fixture's customer_ref returns it |
fixture_not_found, fixture_ref_invalid, card_number_in_field |
get_customer |
The fixture customer, schema-valid | fixture_not_found, schema_invalid |
list_transactions |
Schema-valid, every listed reference accepted | schema_invalid, card_number_in_field |
get_transaction |
The fixture charge belongs to the fixture customer, is in a currency describe lists, its refunds[] do not exceed it, and it is succeeded, not disputed, refund_route: provider, with something left to refund |
fixture_mismatch, currency_not_declared, refunds_exceed_amount, fixture_not_refundable |
get_subscription |
With cancel_subscription: the fixture subscription belongs to the fixture customer and can still be cancelled |
fixture_mismatch, fixture_not_cancellable |
refund_dry_run |
The over-refund dry run is refused with amount_exceeds_refundable and retryable: false, the others answer would_succeed, and the charge does not change |
over_refund_not_refused, refusal_code_mismatch, retryable_mismatch, dry_run_refused, dry_run_changed_state |
cancel_dry_run |
With cancel_subscription: the dry runs answer would_succeed and the subscription does not change |
dry_run_changed_state |
unknown_operation |
get_operation for a key never sent answers unknown |
not_unknown |
outcome_callback |
Skipped while callbacks are off. With them on: describe declares outcome_callbacks and a callback signed with the current srmpc_ secret has been verified since that secret was issued. Send one test event to the callback URL (any key; one we do not hold is accepted and ignored) |
capability_not_declared, no_verified_callback |
mcp_profile |
MCP only, right after describe: every required tool listed once, plus the capability tools describe declares, all with compatible schemas (section 9). If it fails, no dry run is sent |
mcp_tool_missing, mcp_tool_duplicated, mcp_schema_incompatible, mcp_schema_too_complex, mcp_tool_list_too_long |
payment_attempts |
With the capability: one page of at most 10 attempts from the last 24 hours, schema-valid and in order. Skipped when describe does not declare payment_attempts |
schema_invalid, card_number_in_field |
11. Running the reference fake locally
merchantfake is a working implementation of both bindings, with demo data and fault injection,
that Chargeless uses in its own tests. It is development-only (it refuses to start without
SR_ENV=development) and must never be deployed. From the sr-platform directory of a Chargeless
source checkout:
SR_ENV=development FAKEMERCHANT_SIGNING_SECRET=srmps_dev_secret_0123456789 go run ./cmd/fakemerchant| What | Where |
|---|---|
| REST | http://localhost:12112 |
| MCP | http://localhost:12112/mcp |
| Dashboard (customers, charges, armed faults, incoming calls) | http://localhost:12112/ |
Optional environment variables:
| Variable | Purpose |
|---|---|
FAKEMERCHANT_ADDR |
The listen address (default 127.0.0.1:12112) |
FAKEMERCHANT_BASE_PATH |
A base path such as /supportroute |
FAKEMERCHANT_BEARER_TOKEN |
A bearer token to require |
FAKEMERCHANT_EDGE_HEADER_NAME, _VALUE |
An edge header to require |
FAKEMERCHANT_PENDING_DELAY |
How long a pending outcome takes to settle (default 3 s) |
FAKEMERCHANT_MCP_JSON=true |
JSON MCP responses instead of an event stream |
The fake's describe declares partial refunds and both cancellation capabilities, but not outcome
callbacks. The standalone fake sends no callbacks.
Run the kit against it:
export PROVIDERCHECK_SANDBOX_SIGNING_SECRET=srmps_dev_secret_0123456789
go run ./cmd/providercheck -sandbox -allow-private -base-url http://localhost:12112 \
-tenant 0199a3c1-0a0b-7c0d-8e0f-101112131415 -customer cus_1001 -transaction ch_1001 -subscription sub_1001
# MCP: add -transport mcp and use -base-url http://localhost:12112/mcpReset the fake between runs, because the kit cancels sub_1001.
The control API takes JSON bodies with Content-Type: application/json:
curl -s -X POST localhost:12112/_fake/reset -H 'Content-Type: application/json' -d '{}'
curl -s -X POST localhost:12112/_fake/faults -H 'Content-Type: application/json' \
-d '{"operation":"refund","kind":"refuse","code":"merchant_hold","message":"held by finance"}'POST /_fake/faultsarms a fault for one operation, or*. Kinds includelatency,timeout,rate_limited,server_error,in_progress,redirect,refuse,pending,server_error_after_execute,wrong_amount,invalid_body,decimal_amount,card_number_in_field,unknown_operation,succeed_after_failure,retryable_mismatch(the next error answer's flag inverted), and the contract breaks the kit must catch (accept_bad_signature,dry_run_executes,replay_executes,over_refund, and others).DELETE /_fake/faultsclears them.POST /_fake/signing-secretswith{"signing_secret": "…"}deploys another signing secret beside the one the fake holds, as a rotation does (section 3).DELETE /_fake/signing-secretswith{"id": n}stops accepting secret numbern.GET /_fake/statelists the secrets by number, never by value.POST /_fake/settlesettles pending outcomes now.POST /_fake/operations/{key}/reversereverses a succeeded refund.GET /_fake/stateshows the state.
To see Chargeless itself work against the fake, run the local demo (docs/local-demo.md).
12. Checklist before go-live
- Every required operation answers. With
cancel_subscription,get_subscriptionandcancel_subscriptionanswer too. -
describedeclares exactly what you support,"1.0","1.1"or"1.2", your currencies and the attestation. Withpayment_attempts,list_payment_attemptspages by (updated_at,attempt_ref), keepsattempted_atstable, and carries no card number. - Every request's signature is verified over the raw bytes, against your public host, with a
5-minute tolerance and a constant-time comparison. Failures get
401. - Your production endpoint refuses the
srmps_sandbox secret. - Idempotency works per key: replay,
409 idempotency_conflict,409 in_progress, and keys kept at least 30 days. - Your system refuses over-refunds itself, has one effect per call, and refunds only to the original payment method.
- Dry runs change nothing and store no key.
-
get_operationnever answers404.unknownis used only for keys never received,succeededandfailedare final, and reversals are reported asfailedwithreversed_at. -
refunds[]andprior_refundscarry Chargeless's key byte for byte, andnullfor refunds made elsewhere. - Amounts are integers in ISO minor units. References follow section 7.
- Reads answer well under 2.5 s and dry runs under 10 s.
- Every egress IP of the environment is allowlisted. Your certificate is from a public CA, on 443, with no redirects. With mTLS, you pinned the CA.
- Error envelopes carry
retryableas section 5 maps it. - The kit passed against your sandbox in the last 30 days, and you uploaded its sealed report.
- Fixtures are designated, and the live contract check passed in the environment you go live in.
- Your rules are written in your Chargeless policy, not enforced as refusals, and tried against real transactions.
- You log
X-SR-Request-IdandX-SR-Correlation-Id, and you watch for401s. Chargeless alerts on them too (section 3). - Optional: outcome callbacks are declared, the
srmpc_secret is deployed, and one test event was verified.
13. Versioning and change notices
- Contract versions.
- This guide covers contract
1.2.1.0and1.1implementations stay valid:1.1only adds the optionallist_payment_attemptsread and thepayment_attemptscapability, and1.2only adds optional subscription fields (canceled_at,ended_at,price,plan_name). Later 1.x revisions only add. - Chargeless sends only what the revision you declare in
describedefines, and ignores fields it does not know in your answers. - A change that would break a v1 implementation is never made under v1.
- Every change, including description-only edits, is recorded in
CHANGELOG.md. This is one more reason to$refthe schemas rather than copy them.
- This guide covers contract
- Notices from Chargeless.
- Egress IP changes: at least 30 days ahead, in the panel and by email to your owners and admins.
- Client certificate CA changes: at least 90 days ahead.
- The client certificate itself is rotated yearly without notice. Pin the CA, not the certificate.
- Changes you make.
- Rotating the signing secret is staged and takes effect when you activate it. After a rotation of the callback secret, the previous value keeps working for 7 days. "Rotate now" applies at once, for a leaked secret (section 3).
- On a live workspace, a new transport, base URL or mTLS setting must pass the contract check before it is saved. A new transport also needs an uploaded kit report for it (section 10).
- A new capability is used only after a passing check records it.
- The choice of your own system as payment provider is fixed once the workspace is live.