Skip to main content

Jamm SDK for Python

Lightweight Python client for the Jamm API. Focused on:

  • Straightforward auth (client credentials OAuth2)
  • Clean dict responses (flattened where practical)
  • Helpful error metadata (error_type, error_code, decoded debug values)
  • Simple pagination (lazy charge iterator)

Installation

Production (when published):

pip install jamm-sdk

Local development (editable):

pip install -e .

Quick Start

import jamm

client = jamm.configure(
    client_id="your_client_id",
    client_secret="your_client_secret",
    env="staging",  # the sandbox; use "production" for live money
)

print(client.healthcheck())

Configuration

The SDK can be configured using environment variables or programmatically:

Programmatic Configuration

from jamm import JammClient, ClientConfig

config = ClientConfig(
    client_id="your_client_id",
    client_secret="your_client_secret",
    environment="staging",
)

client = JammClient(config)

environment selects the hosts, so it is the only setting needed to move between sandbox and live:

environment API host Who it is for
staging api.staging.jamm-pay.jp The sandbox. Staging credentials are issued alongside production ones.
production (default) api.jamm-pay.jp Live money.

client_secret is held as a pydantic SecretStr, so printing or logging the config — or a traceback that captures it — shows ********** rather than the credential. Read the value back with config.client_secret.get_secret_value().

API version

Every request to the Jamm API carries a Jamm-API-Version header. It defaults to the dated API version this SDK was built against, exposed as jamm.apiversion.API_VERSION. To stay on the version you integrated against, pass api_version to jamm.configure:

client = jamm.configure(
    client_id="your_client_id",
    client_secret="your_client_secret",
    env="production",
    api_version="2026-08-26",
)

An empty api_version means the built-in version. Any other malformed date, or one newer than it, raises ValueError at configure time; upgrade the SDK to use a newer API version. The API rejects a version it no longer serves; the error lists the versions it does. Responses to a pinned older version follow that version's shape, so fields added since come back empty in this SDK's types.

Every versioned response echoes what served it in two headers: Jamm-API-Version is the version, and Jamm-API-Version-Source is how it was chosen: header (the version this SDK sent), pin (your account's pinned version), default (no header, and no pin we could read), or forced (Jamm temporarily served its newest shape, which no dated version describes, so Jamm-API-Version is omitted). A request rejected before a version is chosen carries neither.

OAuth2 token requests are excluded: they go to the identity service, which is not versioned.

Features Overview

Domain Highlights
Healthcheck Ping connectivity quickly
Customers CRUD + contract; normalized: link_initialized, bank_information, status
Payments On-session / off-session async flows
Refunds Full, partial, and cancel-only refunds
Contracts Fetch existing customer contract
Charges Get, list, lazy iterator charges.iter()
Banking Bank search, branches (get/search/list)
Webhooks Verify + parse in one step; typed content per event
Platform Call the API on behalf of a merchant
Types Enum constants and comparison helpers
Errors Unified ApiError with rich metadata

API Reference

Health Check

# Verify API connectivity -- returns the parsed response body, raises ApiError if down
response = client.healthcheck()
print(response)

# Or as a boolean, for a readiness probe
client.health.check()

Customer Management

buyer_data = {
    "email": "customer@example.com",
    "name": "John Doe",
    "katakana_last_name": "ドウ",
    "katakana_first_name": "ジョン",
    "address": "123 Tokyo Street, Shibuya",
    "birth_date": "1990-01-01",
    "gender": "male",
    "force_kyc": False,
    "metadata": {"source": "api"}
}

customer = client.customers.create(buyer=buyer_data)
print(customer["id"])  # flattened
customer = client.customers.get("cus-customer_id_here")
updated = client.customers.update(
    customer_id="cus-customer_id_here",
    data={"name": "Updated Name", "metadata": {"updated": True}},
)
contract = client.customers.get_contract("cus-customer_id_here")
delete_result = client.customers.delete("cus-customer_id_here")

Payment Processing

from datetime import datetime, timedelta, timezone
# Build the timestamp in UTC. datetime.now() returns local time, so suffixing it with
# "Z" would mislabel it and shift the expiry by your machine's offset.
expires_at = (datetime.now(timezone.utc) + timedelta(days=2)).strftime(
    "%Y-%m-%dT%H:%M:%SZ"
)

# Off-session charges are asynchronous: this returns immediately with a charge ID and
# the charge settles in the background. Poll client.charges.get(charge_id) for the
# outcome, or wait for the webhook. idempotency_key is auto-filled with a fresh UUID
# when omitted, which is not the same as a safe retry: re-calling after a lost response
# generates a different key and charges twice. Pass your own key and reuse it to retry.
off_session = client.payments.off_session_async(
    customer_id="cus-customer_id_here",
    price=1000,
    description="Monthly subscription",
    expires_at=expires_at,
    idempotency_key="order-2026-001",  # optional
)

on_session = client.payments.on_session(
    customer_id="cus-customer_id_here",
    price=1000,
    description="One-time payment",
    redirect_urls={
        "success_url": "https://yoursite.com/success",
        "failure_url": "https://yoursite.com/cancel",
    },
    expires_at=expires_at,
)

new_customer_session = client.payments.on_session(
    buyer=buyer_data,
    redirect_urls={
        "success_url": "https://yoursite.com/success",
        "failure_url": "https://yoursite.com/cancel",
    },
    expires_at=expires_at,
)

new_customer_with_charge = client.payments.on_session(
    buyer=buyer_data,
    price=1000,
    description="Initial payment",
    redirect_urls={
        "success_url": "https://yoursite.com/success",
        "failure_url": "https://yoursite.com/cancel",
    },
    expires_at=expires_at,
)

Contract Management

contract = client.contracts.get("cus-customer_id_here")
if contract:
    print("Contract found")
else:
    print("No active contract")

Charge Operations

# Returns None when the charge does not exist (404).
# A rejected request (400) or an authorization failure (403) still raises ApiError.
charge = client.charges.get("trx-charge_id_here")
page = client.charges.list(customer_id="cus-customer_id_here", page_size=25)
for c in client.charges.iter(customer_id="cus-customer_id_here", page_size=100, limit=500):
    process(c)

Refunds

A refund is always processed asynchronously. A successful response means the request was accepted; the outcome arrives by webhook (refund_succeeded or refund_failed). If the same-day cancellation window has not passed the charge is cancelled directly, otherwise a bank transfer refund is created.

# Full refund
result = client.payments.refund("trx-charge_id_here")
print(result["chargeId"], result["refundId"])

# Partial refund, in JPY
client.payments.refund("trx-charge_id_here", amount=500)

# Cancel only -- do not fall back to a bank transfer refund
client.payments.refund("trx-charge_id_here", cancel_only=True)

Platform Mode

Platform credentials can call the API on behalf of a merchant. Configure with platform=True, then pass merchant to any operation:

client = jamm.configure(
    env="develop",
    client_id="your_platform_client_id",
    client_secret="your_platform_client_secret",
    platform=True,
)

client.healthcheck(merchant="mer-merchant_id_here")
client.customers.create(buyer_data, merchant="mer-merchant_id_here")
client.payments.off_session_async("cus-customer_id_here", 1000, merchant="mer-merchant_id_here")
client.payments.refund("trx-charge_id_here", merchant="mer-merchant_id_here")

Platform mode authenticates against a separate identity service, so it requires platform credentials — merchant credentials will not work. Passing merchant without platform=True, or with a malformed merchant ID, raises ValueError before any request is sent.

Enum Constants

Enums arrive as strings over REST and as integers on webhooks. jamm.types carries the constants and comparison helpers that bridge the two, so you never hand-write the wire strings:

import jamm

if jamm.types.error_type_equals(
    jamm.types.ErrorType.ERROR_TYPE_KYC_REJECTED, error.error_type
):
    ...

if jamm.types.api_source_equals(
    jamm.types.ChargeMessageApiSource.API_SOURCE_ON_SESSION, charge.api_source
):
    ...

Available: ErrorType, EventType, ChargeMessageStatus, ChargeMessageApiSource, KycStatus, PaymentAuthorizationStatus, ContractStatus, DepositType, OnSessionPaymentErrorCode, each with a matching *_equals helper.

Either side may be an enum constant or its full wire name. Prefix-stripped short names ("AUTHORIZED") are not accepted — no suffix rule can distinguish a constant from a sibling that ends with it, and AUTHORIZED matching NOT_AUTHORIZED would invert an authorization check. A value the SDK does not recognize matches no constant.

Webhooks

verify_and_parse is the only entry point that verifies: it checks the HMAC signature over the exact received bytes and parses in one step.

Webhook.parse is still public and does not verify. It parses unverified payloads for known event types — it still raises on invalid JSON and on an event type it does not recognise, but it checks no signature. Never use its output for a trust decision; reach for it only when you have already verified the bytes yourself.

message = jamm.Webhook.verify_and_parse(raw_request_body, client_secret)

if jamm.types.event_type_equals(
    jamm.types.EventType.EVENT_TYPE_REFUND_SUCCEEDED, message.event_type
):
    charge = message.content
    print(charge.id, charge.status, charge.refund_id)
    print(charge.refund.amount_refunded, charge.refund.jamm_fee)

    # Your own map from charge creation. Always a dict; `{}` when there is none.
    for key, value in charge.metadata.items():
        print(key, value)

Content is typed per event: ChargeMessage for charge and refund events, ContractMessage for CONTRACT_ACTIVATED, and UserAccountMessage for USER_ACCOUNT_DELETED. Refund events arrive as a nested {transaction, refund} wrapper and are flattened onto ChargeMessage, with the refund's rfd- id on refund_id and the details on refund.

Fields the backend adds later are never fatal, and do not need an SDK release to be readable: every content class is a read-only mapping over its payload.

charge["a_field_this_release_predates"]   # -> its value; KeyError if absent
charge.get("maybe_missing")               # -> None if absent
dict(charge)                              # the content mapping (flattened, for refunds)

For refund events that mapping is the flattened charge, not the {transaction, refund} wrapper the backend sent.

Attribute access stays strict: charge.typo raises AttributeError rather than returning None. Top-level mutation is refused — charge["x"] = 1 and del charge["x"] raise TypeError, charge.update(...) raises AttributeError.

That guarantee stops at the top level. Nested values that came from the payload are shared, not copied — dict(charge) is shallow — so charge.metadata["k"] = v succeeds and every other reader of that payload sees it. When the charge carried no metadata, charge.metadata is a fresh {} and writes stay local, so a metadata-less charge will not show you the difference. Use copy.deepcopy(dict(charge)) to edit nested values.

Banking Operations

bank = client.banks.get("0001")
search_results = client.banks.search("みずほ")
branch = client.bank_branches.get("0001", "001")
branches = client.bank_branches.search("0001", "東京")
all_branches = client.bank_branches.list_for_bank("0001")

Authentication

OAuth2 client credentials handled automatically once configured.

Development Setup

python -m venv venv
source venv/bin/activate
pip install -e .

Running Tests

Unit tests (no credentials, no network):

python -m pytest tests

End-to-end tests against a live environment:

export MERCHANT_CLIENT_ID=your_client_id
export MERCHANT_CLIENT_SECRET=your_client_secret
python test.e2e/run_tests.py

There is also a manual walkthrough that exercises every service and prints a report:

export MERCHANT_CLIENT_SECRET=your_client_secret
python test.e2e/manual_smoke.py --client-id YOUR_ID --env staging

Abbreviated example output:

✅ Healthcheck ok
✅ Customer created
✅ Off-session payment created
✅ On-session payment created

Helper: generate_buyer()

from faker import Faker
import time

def generate_buyer(base_email=None, name=None, force_kyc=False):
    """Generate realistic buyer data for testing"""
    fake_en = Faker("en_US")
    fake_jp = Faker("ja_JP")
    timestamp = int(time.time())

    return {
        "email": f"{fake_en.user_name()}+{timestamp}@jamm-pay.jp",
        "name": name or fake_en.name(),
        "katakana_last_name": fake_jp.last_kana_name(),
        "katakana_first_name": fake_jp.first_kana_name(),
        "address": fake_jp.address(),
        "birth_date": fake_en.date_of_birth(minimum_age=20, maximum_age=70).strftime("%Y-%m-%d"),
        "gender": fake_en.random_element(["male", "female"]),
        "force_kyc": force_kyc,
        "metadata": {"source": "faker_generated", "timestamp": str(timestamp)}
    }

# Usage
buyer_data = generate_buyer()
customer = client.customers.create(buyer=buyer_data)

Error Handling

Every failed SDK call raises jamm.ApiError.

Field Meaning
code HTTP status (0 = network/transport)
message Short description
details Parsed JSON body
error_type Internal debug enum-like (e.g. ERROR_TYPE_PAYMENT_CHARGE_FAILED)
error_code High-level code (internal, invalid, etc.)
debug_values All collected debug markers
decoded_values Base64-decoded detail values
from jamm import ApiError
try:
    client.charges.get("trx-nonexistent")
except ApiError as e:
    print(e.code, e.message, e.error_type)
    if e.error_code:
        print("error_code:", e.error_code)
    if e.decoded_values:
        print("decoded:", e.decoded_values)

Network issues surface with code == 0 and details containing {"network_error": True}.

Notes & Next Steps

Planned (non-breaking): enum wrapper for error_type, helper predicates, optional typed response models. Contributions welcome.

Metadata

Release files for jamm 0.9.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jamm 0.9.0
File Size Uploaded
jamm-0.9.0.tar.gz 116.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for jamm 0.9.0
File Interpreter ABI Platform
jamm-0.9.0-py3-none-any.whl Python 3 none any Details

Total release size: 250.8 kB

Release files / jamm-0.9.0.tar.gz

Download URL jamm-0.9.0.tar.gz
Size 116.5 kB
Tags Source
SHA-256 checksum
How to use checksums
d955e7bb21478dea9180bd8002fe86777357f2ea243939b3937f5c013df145b1
BLAKE2b-256 checksum
How to use checksums
b8fb99c9ad756554e22c21a7cec5ccdddaa73b6f2a0c37a398cde20742baddc7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.17

Release files / jamm-0.9.0-py3-none-any.whl

Download URL jamm-0.9.0-py3-none-any.whl
Size 134.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2f334e9147bcf3f04177ee892752e3859371e238ff2785efebbf19cc658da765
BLAKE2b-256 checksum
How to use checksums
7f37dbc5660c2d34d98cff7854db8a9e72533a0295cde55132774161b96051a1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.17

Release history Release notifications | RSS feed

This release

0.9.0 This release

2 release files

0.8.0

2 release files

0.3.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page