|
StripeKit 0.1.1
Stripe integration toolkit for C++
|
An adapter connects StripeKit to a web framework. StripeKit builds the requests, verifies the signatures, and decides what should happen; the adapter supplies the two things that inevitably depend on your framework: sending HTTPS requests, and owning the route Stripe posts webhooks to.
StripeKit currently includes one adapter: Drogon.
| Piece | Required | Purpose |
|---|---|---|
| stripekit::HTTPClient | Yes | Sends requests to Stripe's REST API. |
| A webhook route | Yes | Receives deliveries and passes the raw body to StripeKit. |
| A factory function | Recommended | One call that hands back a ready stripekit::StripeClient. |
| stripekit::Logger | Optional | Bridges StripeKit's diagnostics into the framework's log. |
Nothing else is needed. Request building, authentication headers, signature verification, and de-duplication are all core behaviour and are not the adapter's business.
The Drogon adapter is a useful reference throughout: see src/adapters/drogon/ for a complete implementation of everything below.
Derive from stripekit::HTTPClient and implement both methods. They receive a stripekit::HttpRequest and produce a stripekit::HttpResponse.
Resolve the path. stripekit::HttpRequest::path is relative, like /v1/customers. Join it to Stripe's base URL, https://api.stripe.com.
Send the body as-is. stripekit::HttpRequest::body is already encoded as application/x-www-form-urlencoded, which is what Stripe's API expects. Do not re-encode it or convert it to JSON.
Authenticate. Set the Authorization header to Bearer followed by a space and your Stripe secret API key. The key is configuration your adapter holds, not part of the request. Never log it.
Apply the options. stripekit::HttpRequest::options carries the per-call settings, and honouring them is what makes safe retries work:
Report the response, not an error. A 4xx or 5xx from Stripe is a normal outcome: fill in stripekit::HttpResponse::status_code and stripekit::HttpResponse::body and return it. StripeKit parses Stripe's error document and throws stripekit::StripeApiException itself. Reserve exceptions for problems that stopped you reaching Stripe at all — DNS failures, TLS errors, timeouts.
Lower-case the response header keys. StripeKit looks them up in lower case.
The callback form must invoke completion exactly once, on every path including the error paths. Calling it twice corrupts the coroutine that may be waiting on it; never calling it hangs the request forever.
stripekit::OnceCallback exists for this. Wrap the callback and let it enforce the rule:
Both methods may be called from any thread and several calls may be in flight at once, so the implementation must be thread-safe.
Applications should not have to assemble the pieces by hand. Offer one function that takes your options and returns a ready client:
Internally it constructs your stripekit::HTTPClient and wraps it in a stripekit::StripeApiClient, which implements stripekit::StripeClient on top of whatever transport you provide. The returned object owns the HTTP client, so the caller has a single thing to keep alive.
Keep the options struct small: the API key, the base URL (overridable, for testing against a mock), and whatever your framework needs to schedule work.
The webhook side is a route handler. It hands StripeKit the raw request body and the Stripe-Signature header, and turns the outcome into an HTTP status.
The signature is computed over the exact bytes Stripe sent. Any framework convenience that parses, re-serialises, or normalises the body will change those bytes and every signature check will fail. Read the unmodified body.
For the same reason, do not apply request-body transformations to this route: no JSON parsing middleware, no character-set conversion, no whitespace trimming.
stripekit::process_event_once returns a stripekit::webhooks::ProcessEventOnceResult whose stripekit::WebhookResponseAction says what to send:
| Condition | Status | Why |
|---|---|---|
| stripekit::WebhookResponseAction::AcknowledgeSuccess | 200 | Done, or a duplicate that was already done. Stripe stops retrying. |
| stripekit::WebhookResponseAction::RetryLater | 500 | Temporary failure. Stripe will deliver again. |
| stripekit::SignatureVerificationException | 400 | Not from Stripe, or the timestamp is outside tolerance. Never retried. |
| stripekit::ConfigurationException | 400 | The payload was not a usable Stripe event. |
Getting the 400 case right matters: a rejected signature must not be answered with a 5xx, or Stripe will keep resending a request you will keep rejecting.
Stripe expects a response within seconds and treats a slow endpoint as a failed delivery. Return the status as soon as StripeKit gives you the outcome, and do not hold the response open for the application's own slow work.
If your framework has a logging system, implement stripekit::Logger so StripeKit's diagnostics end up in the same place as everything else:
Map stripekit::LogLevel onto your framework's levels, and let should_log() answer honestly so StripeKit can skip building messages that would be discarded.
stripekit::LogContext carries structured fields — request id, event id, webhook type — that are worth emitting as separate fields if your logger supports them, since they are what you search on when tracing a payment.
Do not log secrets. stripekit::is_sensitive_field_name() and stripekit::redact_sensitive_text() are there to help.
stripekit::WebhookDedupStore is not an adapter interface, and an adapter should not supply one. Which store to use is the application's decision, because it depends on the application's database and deployment, not on its web framework. Accept a WebhookDedupStore& in your webhook options and pass it through.
If you do implement one — for a database StripeKit does not support — the important property is that stripekit::WebhookDedupStore::claim() is atomic. Two instances racing on the same event id must produce exactly one stripekit::ClaimOutcome::Claimed. A read-then-write is not good enough; use your database's conditional insert.
Implement release_claim() too: after a handler fails, StripeKit releases its reservation so the next delivery can retry immediately. Delete only an unfinished record with the matching claim time; leave newer reservations and completed records alone.
Point the base URL at a local server and check the requests you produce:
For webhooks, sign a payload with a known secret and confirm a valid delivery returns 200, a tampered body returns 400, and delivering the same event twice runs the handler once. StripeKit's own tests under tests/integration/ cover the Drogon adapter this way and are a reasonable template.
Your application can keep using stripekit::StripeClient and the same webhook handler. What changes, is how you create the client and how your server receives webhooks:
Before using the adapter with real payments, run the tests described above and check these cases:
Then test against a real Stripe sandbox. Check successful and failed invoice payments, subscription changes, disputes, refunds, and repeated webhook deliveries. The existing sandbox tests use Drogon, so add tests for these same cases with your adapter. You can use your own adapter without waiting for StripeKit to ship one for your framework.