| Package | Requires | Sync | Async | |
|---|---|---|---|---|
| Python | beaconbox |
3.10+, one dependency (httpx) | yes | yes |
| PHP | beaconbox/beaconbox-php |
8.1+, ext-curl and PSR interfaces |
yes | no |
Both cover the same twenty operations and are MIT licensed.
# No SDK needed. The API is JSON over HTTPS and every page here has a curl form.pip install beaconboxcomposer require beaconbox/beaconbox-phpWhat they handle
Idempotency, generated per call and reused across the SDK’s own retries, so a connection that dies with the answer in flight cannot become a duplicate email and a duplicate charged text. Pass your own key whenever you have a natural one.
Retries, with exponential backoff and full jitter, on connection failures, 429 and 5xx.
Never on a 4xx, because sending the same wrong request again asks the same question.
Webhook signatures, including the timestamp check and a constant-time comparison.
Pagination, as a generator, so a year of history is not held in memory.
Configuration
# Configuration for a hand-rolled client is three rules:
# - refuse a non-https base URL for anything but localhost
# - never follow redirects (the Authorization header follows them)
# - send an Idempotency-Key on every POST, and reuse it when you retryfrom beaconbox import BeaconBox, RetryPolicy
client = BeaconBox(
api_key="bbx_live_...", # or $BEACONBOX_API_KEY
base_url="https://api.beaconbox.eu", # or $BEACONBOX_BASE_URL
timeout=30.0,
retry_policy=RetryPolicy(max_retries=2),
max_connections=20,
user_agent_suffix="acme-orders/2.1",
)$client = new BeaconBox\BeaconBoxClient(
apiKey: getenv('BEACONBOX_API_KEY'),
baseUrl: 'https://api.beaconbox.eu',
timeoutMs: 30_000,
retryPolicy: new BeaconBox\RetryPolicy(maxRetries: 2),
userAgentSuffix: 'acme-shop/2.1',
);Reuse one client. It holds a connection pool, and in Python it is safe to share between threads.
user_agent_suffix makes your integration identifiable in a support conversation. It is worth
setting.
Turn the SDK’s retries off inside a job runner
Backpressure
A Retry-After is honoured in full rather than clamped to the backoff ceiling, because it is the
server’s own answer to when it will be ready and retrying earlier only earns a second refusal.
Jitter is added on top of it, not sampled from within it, since every worker that hit the
same 429 was handed the same number.
If the server asks for longer than the policy will sit through (30 seconds by default), the SDK
stops instead of retrying early, and raises RateLimitError / RateLimitException with the
server’s number on it so you can schedule a real retry.
The two policies are deliberately identical down to the constants, so a shop running one service in each language gets the same behaviour out of a bad ten minutes.
Concurrency, in Python
max_connections is a ceiling, and exceeding it does not look like one: the overflow queues, and a
queue wait that outlives the pool timeout surfaces as APIConnectionError, never as anything
mentioning a pool. An asyncio.gather over a few hundred pushes makes that easy to hit, so raise
the ceiling to match your fan-out.
Async, in Python
Same client, same signatures, awaited. Nothing blocks the event loop, including retry backoff.
from beaconbox import AsyncBeaconBox, OrderStatus
async with AsyncBeaconBox() as client:
result = await client.messages.push(
recipient_email="buyer@example.com",
subject="Your order has shipped",
body="Tracking XY123456789EE.",
reference="#A-10294",
order_status=OrderStatus.SHIPPED,
idempotency_key="order-4711-shipped",
)
async for message in client.messages.iterate(recipient_email="buyer@example.com"):
print(message.id, message.delivery.opened)Logging
Both are silent until you ask, because a library that writes to stderr of its own accord is a library fighting the host application.
# Nothing to configure. Log what you send and what comes back,
# and keep the Authorization header out of it.import logging
# Standard library logging, under the "beaconbox" logger. The SDK attaches a
# NullHandler and configures nothing else: no basicConfig, no root level.
logging.getLogger("beaconbox").setLevel(logging.DEBUG)// PSR-3, opt-in. Pass any LoggerInterface; pass nothing and it is silent.
$client = new BeaconBox\BeaconBoxClient(
apiKey: getenv('BEACONBOX_API_KEY'),
logger: $monolog,
);DEBUG logs every request and response with status and elapsed time. WARNING logs a retry and
giving up after the last one. Nothing is emitted at INFO or above in normal operation, so a
warning from either SDK always means something went wrong.
Bringing your own HTTP client
In PHP, inject any PSR-18 client. The bundled curl transport keeps one persistent handle for connection reuse, which makes it unsafe to share across coroutines, so Swoole and ReactPHP users should inject.
In Python, pass your own httpx.Client, and then you own closing it. timeout and verify are
refused alongside it, never silently ignored, because accepting a setting while doing nothing
with it is how somebody discovers during an incident that the timeout they set was never applied.
Source
Both SDKs are mirrored to public repositories under beaconbox-eu:
beaconbox-python and beaconbox-php.
