Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
API_KEY=
API_KEY=
E2E_API_KEY=
E2E_BASE_URL=https://testapi.multisafepay.com/v1/
PARTNER_API_KEY=
CLOUD_POS_TERMINAL_GROUP_ID=<terminal_group_id>

# Terminal group API keys — one entry per group_id
TERMINAL_GROUP_API_KEY_GROUP_DEFAULT=
# TERMINAL_GROUP_API_KEY_GROUP_A=
# TERMINAL_GROUP_API_KEY_GROUP_B=
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,47 @@ from multisafepay import Sdk
multisafepay_sdk: Sdk = Sdk(api_key='<api_key>', is_production=True)
```

### Development-only custom base URL override

By default, the SDK only targets:

- `test`: `https://testapi.multisafepay.com/v1/`
- `live`: `https://api.multisafepay.com/v1/`

For local development, a custom base URL can be enabled with strict guardrails:

```bash
export MSP_SDK_BUILD_PROFILE=dev
export MSP_SDK_ALLOW_CUSTOM_BASE_URL=1
```

You can provide the custom base URL either via environment variable or via the SDK argument.

Environment variable option:

```bash
export MSP_SDK_CUSTOM_BASE_URL="https://dev-api.example.com/v1"
```

SDK argument option:

```python
from multisafepay import Sdk

sdk = Sdk(
api_key="<api_key>",
is_production=False,
base_url="https://dev-api.example.com/v1",
)
```

Precedence when both are set:

- The explicit SDK argument base_url takes priority.
- If base_url is not passed, MSP_SDK_CUSTOM_BASE_URL is used.

In any non-dev profile (including default `release`), custom base URLs are blocked and the SDK will only use `test/live` URLs.

## Examples

Go to the folder `examples` to see how to use the SDK.
Expand All @@ -93,6 +134,24 @@ make lint
make test
```

### E2E target environment

By default, E2E tests target `https://testapi.multisafepay.com/v1/`.

Use dedicated E2E variables instead of the general SDK variables:

```bash
export E2E_API_KEY="<test_api_key>"
export E2E_BASE_URL="https://testapi.multisafepay.com/v1/" # optional
make test-e2e
```

`E2E_BASE_URL` is optional and can point to any HTTPS base URL used for E2E.
When omitted, E2E defaults to `testapi.multisafepay.com`.

The e2e suite does not use the shared `API_KEY` variable or the shared `MSP_SDK_*`
custom base URL settings.

## Support

Create an issue on this repository or email <a href="mailto:integration@multisafepay.com">
Expand Down
89 changes: 89 additions & 0 deletions examples/event_manager/subscribe_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Create a Cloud POS order and subscribe to its event stream."""

import os
import time

from dotenv import load_dotenv
from multisafepay import Sdk
from multisafepay.api.paths.orders.request import OrderRequest
from multisafepay.client import ScopedCredentialResolver

# Load environment variables from a .env file
load_dotenv()

DEFAULT_ACCOUNT_API_KEY = (os.getenv("API_KEY") or "").strip()
PARTNER_AFFILIATE_API_KEY = (os.getenv("PARTNER_API_KEY") or "").strip()
TERMINAL_GROUP_DEFAULT_API_KEY = (
os.getenv("TERMINAL_GROUP_API_KEY_GROUP_DEFAULT") or ""
).strip()
CLOUD_POS_TERMINAL_GROUP_ID = os.getenv(
"CLOUD_POS_TERMINAL_GROUP_ID",
"Default",
)

if __name__ == "__main__":
if not TERMINAL_GROUP_DEFAULT_API_KEY:
raise RuntimeError(
"TERMINAL_GROUP_API_KEY_GROUP_DEFAULT is required",
)

resolver_bootstrap_api_key = (
DEFAULT_ACCOUNT_API_KEY or TERMINAL_GROUP_DEFAULT_API_KEY
)
resolver_kwargs = {
"default_api_key": resolver_bootstrap_api_key,
"terminal_group_api_keys": {
CLOUD_POS_TERMINAL_GROUP_ID: TERMINAL_GROUP_DEFAULT_API_KEY,
},
}
if PARTNER_AFFILIATE_API_KEY:
resolver_kwargs["partner_affiliate_api_key"] = (
PARTNER_AFFILIATE_API_KEY
)

credential_resolver = ScopedCredentialResolver(**resolver_kwargs)

multisafepay_sdk = Sdk(
is_production=False,
credential_resolver=credential_resolver,
)
order_manager = multisafepay_sdk.get_order_manager()
event_manager = multisafepay_sdk.get_event_manager()

# Temporary override for local runs; comment this line to force literal placeholder.
# terminal_id = os.getenv("CLOUD_POS_TERMINAL_ID", "<terminal_id>")

order_id = f"cloud-pos-{int(time.time())}"

order_request = (
OrderRequest()
.add_type("redirect")
.add_order_id(order_id)
.add_description("Cloud POS order")
.add_amount(100)
.add_currency("EUR")
.add_gateway_info(
{
"terminal_id": terminal_id,
},
)
Comment on lines +64 to +69
)

create_response = order_manager.create(
order_request,
terminal_group_id=CLOUD_POS_TERMINAL_GROUP_ID,
)
order = create_response.get_data()

if order is None:
raise RuntimeError("Order creation did not return order data")

print(f"Created Cloud POS order: {order.order_id}")
print("Listening for events. Press Ctrl+C to stop.")

try:
with event_manager.subscribe_order_events(order, timeout=45.0) as stream:
for event in stream:
print(event)
except KeyboardInterrupt:
print("Stream interrupted by user.")
90 changes: 90 additions & 0 deletions examples/order_manager/cancel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Create a Cloud POS order, wait 5 seconds, and cancel it."""

import os
import time

from dotenv import load_dotenv

from multisafepay import Sdk
from multisafepay.api.paths.orders.request import OrderRequest
from multisafepay.client import ScopedCredentialResolver

# Load environment variables from a .env file
load_dotenv()

DEFAULT_ACCOUNT_API_KEY = (os.getenv("API_KEY") or "").strip()
PARTNER_AFFILIATE_API_KEY = (os.getenv("PARTNER_API_KEY") or "").strip()
TERMINAL_GROUP_DEFAULT_API_KEY = (
os.getenv("TERMINAL_GROUP_API_KEY_GROUP_DEFAULT") or ""
).strip()
CLOUD_POS_TERMINAL_GROUP_ID = os.getenv(
"CLOUD_POS_TERMINAL_GROUP_ID",
"Default",
)

if __name__ == "__main__":
if not TERMINAL_GROUP_DEFAULT_API_KEY:
raise RuntimeError(
"TERMINAL_GROUP_API_KEY_GROUP_DEFAULT is required",
)

resolver_bootstrap_api_key = (
DEFAULT_ACCOUNT_API_KEY or TERMINAL_GROUP_DEFAULT_API_KEY
)
resolver_kwargs = {
"default_api_key": resolver_bootstrap_api_key,
"terminal_group_api_keys": {
CLOUD_POS_TERMINAL_GROUP_ID: TERMINAL_GROUP_DEFAULT_API_KEY,
},
}
if PARTNER_AFFILIATE_API_KEY:
resolver_kwargs["partner_affiliate_api_key"] = (
PARTNER_AFFILIATE_API_KEY
)

credential_resolver = ScopedCredentialResolver(**resolver_kwargs)

multisafepay_sdk = Sdk(
is_production=False,
credential_resolver=credential_resolver,
)
order_manager = multisafepay_sdk.get_order_manager()

# Temporary override for local runs; comment this line to force literal placeholder.
terminal_id = os.getenv("CLOUD_POS_TERMINAL_ID", "<terminal_id>")

order_request = (
OrderRequest()
.add_type("redirect")
.add_order_id(f"cloud-pos-cancel-{int(time.time())}")
.add_description("Cloud POS cancel order")
.add_amount(100)
.add_currency("EUR")
.add_gateway_info(
{
"terminal_id": terminal_id,
},
)
)

create_response = order_manager.create(
order_request,
terminal_group_id=CLOUD_POS_TERMINAL_GROUP_ID,
)
order = create_response.get_data()

if order is None or not order.order_id:
raise RuntimeError("Order creation did not return order_id")

order_id = order.order_id
print(f"Created Cloud POS order: {order_id}")
print("Waiting 5 seconds before cancel...")
time.sleep(5)

cancel_response = order_manager.cancel_transaction(
order_id,
terminal_group_id=CLOUD_POS_TERMINAL_GROUP_ID,
)

print(f"Canceled Cloud POS order: {order_id}")
print(cancel_response.get_data())
91 changes: 91 additions & 0 deletions examples/order_manager/cloud_pos_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Create a Cloud POS order and print its event stream credentials."""

import os
import time

from dotenv import load_dotenv
from multisafepay import Sdk
from multisafepay.api.paths.orders.request import OrderRequest
from multisafepay.client import ScopedCredentialResolver

# Load environment variables from a .env file
load_dotenv()

DEFAULT_ACCOUNT_API_KEY = (os.getenv("API_KEY") or "").strip()
PARTNER_AFFILIATE_API_KEY = (os.getenv("PARTNER_API_KEY") or "").strip()
TERMINAL_GROUP_DEFAULT_API_KEY = (
os.getenv("TERMINAL_GROUP_API_KEY_GROUP_DEFAULT") or ""
).strip()
CLOUD_POS_TERMINAL_GROUP_ID = os.getenv(
"CLOUD_POS_TERMINAL_GROUP_ID",
"Default",
)

if __name__ == "__main__":
if not TERMINAL_GROUP_DEFAULT_API_KEY:
raise RuntimeError(
"TERMINAL_GROUP_API_KEY_GROUP_DEFAULT is required",
)

# Reuse one SDK for mixed traffic. The resolver is the source of truth for
# which key is used per endpoint/scope.
resolver_bootstrap_api_key = (
DEFAULT_ACCOUNT_API_KEY or TERMINAL_GROUP_DEFAULT_API_KEY
)
resolver_kwargs = {
"default_api_key": resolver_bootstrap_api_key,
"terminal_group_api_keys": {
CLOUD_POS_TERMINAL_GROUP_ID: TERMINAL_GROUP_DEFAULT_API_KEY,
},
}
if PARTNER_AFFILIATE_API_KEY:
resolver_kwargs["partner_affiliate_api_key"] = (
PARTNER_AFFILIATE_API_KEY
)

credential_resolver = ScopedCredentialResolver(**resolver_kwargs)

multisafepay_sdk = Sdk(
is_production=False,
credential_resolver=credential_resolver,
)
order_manager = multisafepay_sdk.get_order_manager()

terminal_id = "<terminal_id>"
# Uncomment this line to override with CLOUD_POS_TERMINAL_ID from .env.
# terminal_id = os.getenv("CLOUD_POS_TERMINAL_ID", terminal_id)

order_id = f"cloud-pos-{int(time.time())}"

order_request = (
OrderRequest()
.add_type("redirect")
.add_order_id(order_id)
.add_description("Cloud POS order")
.add_amount(100)
.add_currency("EUR")
.add_gateway_info(
{
"terminal_id": terminal_id,
},
)
)

create_response = order_manager.create(
order_request,
terminal_group_id=CLOUD_POS_TERMINAL_GROUP_ID,
)
order = create_response.get_data()

if order is None:
raise RuntimeError("Order creation did not return order data")

print(f"Created Cloud POS order: {order.order_id}")

events_token = order.events_token or order.event_token
events_stream_url = order.events_stream_url or order.event_stream_url

if events_token and events_stream_url:
print("Event stream credentials:")
print(f"EVENTS_TOKEN={events_token}")
print(f"EVENTS_STREAM_URL={events_stream_url}")
Loading
Loading