super_pdp

Thin, dependency-free Ruby client for the SUPER PDP API — send and receive invoices under France's e-invoicing reform (Peppol network).

Stdlib only (net/http + json). No Faraday, no generated model classes.

Unofficial. This is an independent, community-maintained client. It is not affiliated with, endorsed by, or supported by SUPER PDP. "SUPER PDP" and any related marks belong to their respective owners; they are used here only to describe what this gem talks to (nominative use). The gem calls the publicly documented HTTP API — no proprietary code is included — and is provided "as is" under the MIT license, with no warranty. The maintainer may update or yank this gem from RubyGems at any time; pin a version if you depend on it.

Install

gem "super_pdp"

Auth

Two modes, both OAuth2:

# Client credentials — acts on your own data (api-key-like). Token is fetched
# and refreshed automatically.
pdp = SuperPDP.new(client_id: "...", client_secret: "...")

# Bearer token — acts on a user's behalf. You obtain the token yourself via the
# authorization_code flow (/oauth2/authorize) and pass it in.
pdp = SuperPDP.new(access_token: "user-token")

Use

pdp.companies_me
pdp.create_invoice({ en_invoice: { ... } })   # body is a positional Hash
pdp.create_invoice({ en_invoice: { ... } }, expand: %w[events])  # + query params
pdp.invoice(42, expand: %w[en_invoice events])
pdp.download_invoice(42)              # raw bytes (PDF/XML), not JSON
pdp.create_invoice_event(invoice_id: 42, status: "fr:204")

# Cursor pagination handled for you:
pdp.each_item("/invoices", direction: "outgoing").each do |inv|
  puts inv["id"]
end

Named helpers exist for every endpoint (companies, invoices, invoice_events, ereportings, b2c_transactions, b2c_payments, directory_entries, french_directory, validation_reports, oauth2_sessions). For anything not wrapped, drop to the raw verbs:

pdp.get("/invoices", limit: 10)
pdp.post("/some/new/route", { key: "value" })

Responses are parsed JSON (Hash/Array). Non-2xx raises SuperPDP::APIError (#status, #body), or a status-specific subclass you can rescue directly: UnauthorizedError (401/403), NotFoundError (404), RateLimitError (429, with #retry_after in seconds). All subclass APIError, so rescue SuperPDP::APIError still catches everything.

Retries

Transient failures on idempotent requests (GET/DELETE) are retried automatically: HTTP 429 and 502/503/504, plus connection errors. The server's Retry-After header is honored; otherwise it backs off exponentially. POST and PATCH are never retried (a half-applied write is worse than an error).

SuperPDP.new(client_id: "...", client_secret: "...",
             max_retries: 2,    # total attempts = max_retries + 1 (default 2)
             retry_base: 0.5)   # backoff seconds: retry_base * 2**(attempt-1)

Test

bundle exec rake test     # or: bundle exec rake  (runs tests + rubocop)

Contributing

Contributions are welcome — see CONTRIBUTING.md. In short: open an issue to discuss anything non-trivial, then send a PR with bundle exec rake (tests + rubocop) passing.

Releasing (maintainers)

Bump SuperPDP::VERSION, update CHANGELOG.md, then:

bundle exec rake release   # tags the version and pushes the .gem to RubyGems

License

MIT — see LICENSE.txt.