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 refund or cancel_subscription only 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 disputed charge automatically, and never attempts a refund on a charge whose refund_route is unsupported;
    • 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 implement list_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: needs cancel_subscription;
      • outcome_callbacks;
      • payment_attempts (1.1, optional; absent means false): you implement list_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_customers takes one or more of email, phone (E.164), customer_ref, transaction_ref, order_ref and card_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 is 200 with an empty list, never a

    1. 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, closed or restricted;
    • 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 of warning, open, won or lost;
    • 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_ref and customer_ref, the current owner: ownership is decided on it, read fresh;
    • order_ref or null; subscription_ref or null;
    • state: pending, succeeded or failed; this describes the charge itself, not refunds;
    • amount;
    • created_at: the policy's refund window counts from here;
    • description or null: at most 200 characters, shown to staff only;
    • product_type or null, a label;
    • charge_type: initial, renewal, trial_conversion, upgrade, one_time or top_up;
    • processor: a label such as stripe or in_house;
    • refund_route: provider or unsupported;
    • card_last4 or null; card_brand or null;
    • disputed;
    • refunds: every refund that took effect, oldest first, at most 100. Each has at, amount, idempotency_key (Chargeless's key, byte for byte, or null for a refund made any other way) and reversed_at (or null). A pending refund is not listed until it takes effect. A reversed refund stays listed;
    • settlement or 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, canceled or expired. canceled and expired are final;
    • cancel_at_period_end;
    • current_period_end or null; trial_end or null;
    • billing_interval, as {unit, count};
    • plan_ref or null;
    • renewal_count;
    • created_at;
    • last_charge or null, as {transaction_ref, at, amount};
    • optional attributes;
    • revision 1.2, optional (absent means null): canceled_at, ended_at, price (per billing_interval) and plan_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 (a plan_name over 120 characters, a malformed canceled_at or ended_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), reason and dry_run.

  • Refund result:

    • idempotency_key: as received, null only for a dry run;
    • status: succeeded, pending or would_succeed;
    • transaction_ref and amount: as requested;
    • refund_ref or null;
    • processed_at: required with succeeded.
  • Cancellation request: at_period_end, reason and dry_run.

  • Cancellation result:

    • idempotency_key;
    • status;
    • subscription_ref and at_period_end: as requested;
    • effective_at: required with succeeded, null for a dry run or while pending.
  • Operation (get_operation): idempotency_key and status (unknown, pending, succeeded or failed). The other fields depend on the status:

    Status Also carries
    unknown nothing else
    pending or succeeded operation, target_ref, result, updated_at
    failed by a refusal operation, target_ref, refusal: {code, message}, updated_at
    failed by a reversal operation: refund, target_ref, result, reversed_at, updated_at
  • attributes: 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 of duplicate, forgot_to_cancel, unrecognized, not_as_described, service_issue or other. It is for your books only and never changes what the operation does. The set is open, so treat an unknown value as other.

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's next_cursor) and limit (1–200, default 100).
  • Order. Return attempts whose updated_at is at or after updated_since, ascending by (updated_at, attempt_ref), at most limit. next_cursor is present exactly when has_more is 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 same updated_since. An unknown or expired cursor is 400 invalid_request: Chargeless then reads again from its bound without a cursor. A 400 on a first page (no cursor) is a refused request, not an expired cursor.
  • One attempt, one attempted_at. attempt_ref is stable for the attempt's life. attempted_at never changes: a later report with another attempted_at is a contract violation. When anything else changes (a pending attempt that settles, a code added), set a later updated_at, at or after attempted_at, and make the change visible to this read within 60 seconds. succeeded and failed should then stay final.
  • What each attempt carries. Every key is present; a value you do not know is null. status is succeeded, failed or pending. amount is the amount attempted, in the currency attempted. card is null for a non-card attempt; otherwise it carries brand, network, funding (credit, debit, prepaid or unknown), country, bin (6–8 digits, never more), pan_length (12–19, or null), last4, issuer (at most 200 characters, no control characters), wallet and three_ds_result. failure_category is null except on a failed attempt, where it may be null and Chargeless derives it from the decline codes. Decline, network-decline and advice codes are at most 64 characters. risk.score is 0–99 or null. billing_country and ip_country are 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 stores bin truncated: 8 digits only when pan_length is 16 or more, otherwise 6, and card-masks issuer before 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). Answer 429 with Retry-After to 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 describe stays valid.

Money

  • amount_minor is 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.
  • 1999 is valid. 1999.0, 19.99, 1.999e3, "1999" and negative amounts are refused, even when the value is whole.
  • currency is 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):

text
{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:

  1. compute the HMAC over the bytes you received, before parsing anything;
  2. compare in constant time;
  3. 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).

python
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):

text
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=1b03ea29388ff9f1dd2bbf894f1e6e359f0fa1003ca246c678ad485249503070

The 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.

  1. Stage. Settings → Payment provider → Signing secret → "Stage a new secret". The new srmp_ secret is shown once. Nothing changes for your endpoint yet.

  2. Deploy. Let your verifier accept both secrets, and deploy the new one.

  3. 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 unknown answer; 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 also Mcp-* and Last-Event-ID.
    • Values: up to 1,024 printable ASCII characters.
  • 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 3xx and the call fails.
  • 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.

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 timeout with detail validation_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 retryable flag 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. A resource_exhausted answer asks you to retry after a short pause.

4. Idempotency

  • The key. Every refund and cancel_subscription that is not a dry run carries Idempotency-Key: {workspace_id}:{action key}, at most 255 characters of [A-Za-z0-9:_-], for example 0199a3c1-…-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 succeeded for a refund that was pending at the first call.
  • Same key, different parameters. Answer 409 with code idempotency_conflict and execute nothing. Chargeless treats this as final and records it as a security event.
  • Same key while the first call still runs. Answer 409 with code in_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's prior_refunds, exactly as received. That is how Chargeless counts its own refunds once.
  • Keep keys for at least 30 days. get_operation must answer for every key you received in that time.
  • Dry runs ("dry_run": true) carry no key. They validate fully, answer would_succeed or 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, 5xx or 409 in_progress, backing off from 200 ms to 2 s. It honours Retry-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:

  • false for 400, 401, 403, 404, 409 idempotency_conflict and 422;
  • true for 409 in_progress, 429 and 5xx.

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

  1. succeeded and failed are final for a key. A refusal (422) or failed means that no money moved under that key, and none ever will.
  2. The one later change allowed is a reversal: you returned a succeeded refund. Report it from get_operation as failed with reversed_at and the refund's result. Keep the refund in the charge's refunds[] with reversed_at set. 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.
  3. unknown is only for a key you never received. get_operation never answers 404.

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.

  • unknown within 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:

json
{"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 and get_operation settles 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).
  • 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, a or b, 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.

  1. Declare it. Declare "outcome_callbacks": true in describe.

  2. 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}.

  3. Send events. When the operation under one of Chargeless's keys changes (it settled, it was refused, or a succeeded refund was reversed), POST:

    json
    {"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_key is the key exactly as Chargeless sent it.
    • event_id is 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.
  4. Sign it. Sign the request with the callback secret, in the same scheme as section 3:

    text
    {timestamp}\nPOST\n{Chargeless's host}\n/webhooks/merchant/{workspace_id}\n\n{body}
    • The key field is empty.
    • The headers are X-SupportRoute-Timestamp and X-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_id is 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 one tools/call and 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_key argument, inside the signed body; there is no Idempotency-Key header.
  • 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 structuredContent and is validated exactly like a REST body. An error is a result with isError: true whose structuredContent is the error envelope; its code maps to a REST status. A JSON-RPC error counts as a 5xx.
  • Schemas. Every profile tool declares its inputSchema and outputSchema as a $ref to 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.

  1. Configure the sandbox. Put the sandbox signing secret (srmps_…) on your sandbox, and only there.

  2. 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 with partial_refunds, otherwise the whole remainder) and cancels the subscription, so use fresh fixtures for every run.

  3. Run the kit. Secrets come only from the environment.

    sh
    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.json
  4. Read the result.

    • Exit status 0 means every check passed, 1 that a check failed, and 2 that the run could not start.
    • It prints one line per check, with the X-SR-Request-Id of a failing call.
    • It writes a report sealed with a key derived from the sandbox secret.
  5. 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 (the cancel_* lines and one_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 declaring cancel_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 rest or mcp, 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 an srmp_ 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 as 192.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:

    1. a cancellation at period end (with cancel_at_period_end);
    2. a refund one minor unit over the remainder, which must be refused;
    3. one minor unit (with partial_refunds);
    4. the remainder;
    5. an immediate cancellation.

    The check stops at the first change it sees (dry_run_changed_state). Make sure your dry runs never execute.

  • 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:

sh
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:

sh
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/mcp

Reset the fake between runs, because the kit cancels sub_1001.

The control API takes JSON bodies with Content-Type: application/json:

sh
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/faults arms a fault for one operation, or *. Kinds include latency, 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/faults clears them.
  • POST /_fake/signing-secrets with {"signing_secret": "…"} deploys another signing secret beside the one the fake holds, as a rotation does (section 3). DELETE /_fake/signing-secrets with {"id": n} stops accepting secret number n. GET /_fake/state lists the secrets by number, never by value.
  • POST /_fake/settle settles pending outcomes now.
  • POST /_fake/operations/{key}/reverse reverses a succeeded refund.
  • GET /_fake/state shows 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_subscription and cancel_subscription answer too.
  • describe declares exactly what you support, "1.0", "1.1" or "1.2", your currencies and the attestation. With payment_attempts, list_payment_attempts pages by (updated_at, attempt_ref), keeps attempted_at stable, 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_operation never answers 404. unknown is used only for keys never received, succeeded and failed are final, and reversals are reported as failed with reversed_at.
  • refunds[] and prior_refunds carry Chargeless's key byte for byte, and null for 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 retryable as 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-Id and X-SR-Correlation-Id, and you watch for 401s. 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.0 and 1.1 implementations stay valid: 1.1 only adds the optional list_payment_attempts read and the payment_attempts capability, and 1.2 only 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 describe defines, 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 $ref the schemas rather than copy them.
  • 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.