Download the PHP package uengage.io/php-platform-sdk without Composer
On this page you can find all versions of the php package uengage.io/php-platform-sdk. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download uengage.io/php-platform-sdk
More information about uengage.io/php-platform-sdk
Files in uengage.io/php-platform-sdk
Package php-platform-sdk
Short Description PHP SDK for the uEngage platform API (zones, business, audit, auth, events, wallet, upsell, alerts). OAuth2 client_credentials, legacy session exchange, and static-Bearer auth modes with pluggable token caching (APCu / file / in-memory), plus a buffered fail-open publisher for the platform event bus and an alerts client (raise straight to the alerts SQS FIFO queue, list/resolve via HTTP).
License MIT
Informations about the package php-platform-sdk
uengage.io/php-platform-sdk
Backend-issued public/private suggestion tokens and outlet, brand or global admin tokens: upsell token guide.
PHP client SDK for the uEngage platform API. Mirrors the JS SDK
(@uengage.io/platform-sdk) — same five namespaces (zones,
business, audit, auth, wallet), same auth modes, same error envelope.
- Base URL (default):
https://api.platform.uengage.io - PHP: 7.1+
- Deps: ext-curl, ext-json (no Guzzle, no other runtime deps)
Restricted realtime channels
Use $platform->auth->mintRealtimeToken($clientId, $clientSecret, 'delivery-updates', 300)
on your backend after authorizing the current user's access to that channel.
The client must be registered with realtime.tokens:issue. The result is
['token' => ..., 'expiresIn' => 300, 'channel' => 'delivery-updates'] and can be
returned from a non-cacheable same-origin token route. Credentials stay server-side.
Names are case-sensitive identifiers, not paths or wildcards. Tokens permit exactly one name, are valid for 60–14400 seconds (300 by default), and cannot publish or call other platform APIs. Minting does not create a publisher. See the complete integration and authorization guide.
Install
If you have not configured Packagist yet, point at the mirror repo
directly in composer.json:
Quick start
Configuration
Client::create([...]) takes the same options as the JS SDK:
| Option | Type | Default |
|---|---|---|
baseUrl |
string | https://api.platform.uengage.io |
authBaseUrl |
string | {baseUrl}/auth/business |
customerAuthBaseUrl |
string | {baseUrl}/auth/customer |
serviceId + serviceSecret |
string | OAuth2 client_credentials mode |
authToken |
string | static Bearer mode (caller owns freshness) |
session |
['id' => ..., 'token' => ...] |
legacy uEngage session-exchange mode |
getToken |
callable | renews a static vault token (see Vault); not with serviceId |
scope |
string (optional) | space-separated scope list for client_credentials |
actorVia |
string | stamped into audit actor.via |
cache |
TokenCacheInterface |
APCu if loaded, else file-on-disk |
http |
HttpClient |
default (cURL backend) |
eventsTopicArn |
string | SNS topic ARN for the platform event bus |
eventsRegion |
string | defaults to the region inside eventsTopicArn |
eventSource |
string | envelope source field, default legacy-php |
snsPublisher |
SnsPublisherInterface |
default: AwsSnsPublisher (aws/aws-sdk-php) |
alertsQueueUrl |
string | alerts SQS FIFO queue for alerts->raise() |
env |
uat | prod |
picks the default alerts queue |
sqsSender |
SqsSenderInterface |
default: AwsSqsSender (aws/aws-sdk-php) |
Auth modes are mutually exclusive. Picking more than one throws
ConfigException. Picking zero is allowed - the client only works
against public endpoints (the openapi spec).
Env defaults (read by Client::create() when an option is omitted):
UENGAGE_BASE_URL, UENGAGE_AUTH_BASE_URL, UENGAGE_CUSTOMER_AUTH_BASE_URL,
UENGAGE_SERVICE_ID, UENGAGE_SERVICE_SECRET, UENGAGE_SCOPE,
UENGAGE_AUTH_TOKEN, UENGAGE_SESSION_ID, UENGAGE_SESSION_TOKEN,
UENGAGE_ACTOR_VIA, PLATFORM_EVENTS_TOPIC_ARN, PLATFORM_EVENTS_REGION,
PLATFORM_EVENTS_SOURCE, PLATFORM_ALERTS_QUEUE_URL, UENGAGE_ENV.
Events client (platform event bus)
Publishes order-lifecycle events onto the platform's SNS bus. This is the one namespace that does not go through the platform HTTP API — it publishes straight to SNS with the host's AWS credentials (on legacy, the EC2 instance role), so there is no token to mint and no extra hop on the order path.
publish()buffers in memory and returns immediately. The queue is flushed at request end viaregister_shutdown_function, inPublishBatchcalls of up to 10, with short timeouts.- The shutdown flush stays off the request's clock. It calls
fastcgi_finish_request()first where the SAPI has it (PHP-FPM), so the response is already closed before anything talks to SNS, and it is capped atSHUTDOWN_MAX_CHUNKS(5 batches = 50 events) even then — because closing the client still leaves an FPM worker held, and workers are the scarce resource under load. Against a dead bus the worst case is ~15s of worker time, not minutes. Events past the cap are counted and logged, not retried. An explicitflush()is uncapped: that is the caller choosing to wait. - Fail-open, unconditionally.
flush()catches everything, writes toerror_log(), and returns. A bus outage, an expired instance role, or a missing AWS SDK can never fail an order status change. Callflush()yourself and checkfailedCount()if you want to observe failures. - The SDK stamps
id(monotonic ULID),occurredAt,version,domain, andsource. Call sites supply type, payload, and tenant. publish()does throwInvalidArgumentExceptionfor a malformed type, an unmapped domain, a missingtenantId, or list-shapeddata— those are call-site bugs, not runtime conditions, and failing open on them would just fill a DLQ.- Each envelope is JSON-encoded individually before the batch is assembled. Malformed UTF-8 (realistic: customer names come straight out of the legacy DB) is substituted on PHP 7.2+ and, failing that, drops just that one entry with a log — its nine batch siblings still go out. Envelopes over the SNS 256 KB message limit are dropped the same way.
Requires aws/aws-sdk-php
It is a suggested, not a required, dependency: this SDK is installed by consumers that only use zones/business/audit, and a hard requirement would drag the AWS SDK — and its PHP >= 7.2.5 floor — into all of them. Applications that publish events or raise alerts add it themselves:
Without it, publish() still buffers and flush() fails open with an
error_log() entry. Hosts that already own a configured SnsClient can pass
'snsPublisher' => new AwsSnsPublisher($region, $existingClient), or any
SnsPublisherInterface.
Alerts client
Raise operational alerts (low wallet balance, dead payment-gateway credentials, ...) and read / resolve them from a dashboard backend. Two transports:
| Call | Transport | Needs |
|---|---|---|
raise() |
SQS FIFO queue alerts-{env}.fifo |
host AWS identity with sqs:SendMessage on it, aws/aws-sdk-php |
list(), get() |
GET /v1/alerts[/{id}] |
Bearer token with alerts:read |
setStatus() |
POST /v1/alerts/{id}/status |
Bearer token with alerts:write |
Raising
raise() sends the alert straight to the alerts SQS FIFO queue — no SNS, no
event bus, no HTTP call — and otherwise behaves like the events client above:
- Invalid input throws
InvalidArgumentExceptionimmediately — unknowntype, blank or over-longtitle(200) /description(4000, both measured after trimming), missingtenant.parentId/tenant.businessId, a non-boolnotify, or acontextthat is nested, list-shaped, over 50 keys, has a key outside 1–64 characters or a string value over 1024 characters. - Valid alerts are buffered in the alerts client's own queue and sent at
request end (or on
$platform->alerts->flush()) inSendMessageBatchcalls of up to 10, with short timeouts. The shutdown flush closes the response first (fastcgi_finish_request()) and is capped at 5 batches, likeevents. - Sending fails open: an SQS outage, no resolvable queue, a missing AWS SDK
or a denied
sqs:SendMessagegoes toerror_log()and$platform->alerts->failedCount()(cumulative — diff it around aflush()), never to the caller. CLI workers and queue consumers should call$platform->alerts->flush()themselves. - The SDK derives
categoryfrom the type registry (AlertsClient::ALERT_TYPES), mintsid(ULID, returned byraise()), stampsoccurredAt(UTC, no milliseconds) and stringifies tenant ids so7175and"7175"are the same alert. Unknown keys are dropped. - Each message goes out with
MessageGroupId={parentId}#{businessId}#{type}(AlertsClient::messageGroupId()), so one alert's messages are processed strictly in order, andMessageDeduplicationId= the messageid.
Queue URL, first match wins (same order as the TS SDK):
- the
alertsQueueUrloption, elsePLATFORM_ALERTS_QUEUE_URL; - the default for the
envoption, elseUENGAGE_ENV(AlertsClient::ALERT_QUEUE_URLS):uat:https://sqs.ap-south-1.amazonaws.com/571896969292/alerts-uat.fifoprod:https://sqs.ap-south-1.amazonaws.com/822426641710/alerts-prod.fifo
- otherwise (no env, or an unknown one such as
dev) there is no queue and every raised alert is logged and counted as failed at flush.
The AWS region comes from the queue URL host (sqs.<region>.amazonaws.com),
else AWS_REGION. Credentials come from the AWS SDK's default chain. A queue
URL on a non-AWS host (LocalStack, ElasticMQ) is also used as the SQS endpoint.
IAM. The host's AWS identity needs sqs:SendMessage on
arn:aws:sqs:ap-south-1:<account>:alerts-{env}.fifo (SendMessageBatch is
authorised by sqs:SendMessage). The queue lives in the platform account, so a
producer in another AWS account (e.g. the legacy stack) also has to be listed
in the alerts queue's resource policy — the platform grants it to the roles in
its producer role list; ask for your role to be added. The SDK auth modes are
not used for raising. Hosts that own a configured SqsClient can pass
'sqsSender' => new AwsSqsSender(null, $existingClient), or any
SqsSenderInterface.
Matching. The service keys an alert on (businessId, parentId, type).
While an alert with that identity is open or muted, a new message adds to
its count, moves lastSeenAt and updatedAt, and the latest title,
description, context and notify win. Once it is resolved, the next
raise() starts a new alert row (fresh id, count 1). Title, description
and context therefore do not split alerts — put volatile values in context
freely.
Listing and resolving
An alert is {id, parentId, businessId, title, description, type, category, context, notify, status, count, firstSeenAt, lastSeenAt, updatedAt, userIds} plus
resolvedAt/resolvedBy/resolutionComment, mutedAt/mutedBy/muteComment and
reopenedAt/reopenedBy/reopenComment when set. count is the number of
messages received while it was open or muted; firstSeenAt / lastSeenAt are
the occurredAt of its first and latest message; updatedAt is the last change
of any kind. id ({businessId}.{type}#{ulid}) is stable.
userIds lists the dashboard users who can see the alert. The service decides
it once, when the alert row starts, from its audience rules: a rule for the
alert's business + type if one exists, else one for its parent + type, else a
default list. Count bumps never change it, and changing the rules (a service
deploy) only affects alert rows started afterwards. A userId list page may
hold fewer than limit items while nextCursor is still set — keep paging
until it is null.
Status changes: open ↔ muted and open|muted → resolved always work. Reopening
(resolved → open, comment optional) works only while no other open or muted
alert of the same identity exists — if a later raise() already started a new
one, the service answers 409 newer_alert_open with that alert's id:
These use the normal auth modes; the service's token needs alerts:read for
list()/get() and alerts:write for setStatus(). Non-2xx responses throw
Uengage\PlatformSdk\Alerts\AlertsApiException (getStatus(), getBody(),
getErrorCode()).
Upsell client
Cart-aware upsell suggestions. Three calls carry a storefront integration: ask which slots exist, ask what goes in one, report what happened.
Six things that are not obvious from the types:
- An empty
itemslist is a normal outcome, not an error. The price band is a percentage of the cart subtotal, so a small cart legitimately has nothing to suggest. Render nothing — no card, no heading, no empty state. - Send
sectionNameandvegon every cart line. Both are optional in the schema and load-bearing in the engine:vegdrives the dietary rule and only engages when every line carries it, andsectionNamedrives the don't-repeat-a-cart-category rule. Omitting them silently disables both. - Size your layout off
meta.maxItems, notdisplay.maxItems. The first is what the merchant asked for; the second is what the touchpoint policy allowed. A slot configured for 6 reports 3 at the cart and 2 at checkout. display.layoutis opaque — the platform never interprets it. Map it to your own component with a fallback, so a merchant picking a new layout does not need a release.reportEventsnever throws for I/O. A timeout, DNS failure, expired token or 5xx is logged viaerror_logand returns['accepted' => 0], so telemetry cannot take down a cart render. Caller mistakes — a missing tenant, an empty batch, more than 50 events — still throw, because those are bugs to fix rather than conditions to survive.- Ids may be numeric strings.
'5'from$_GET,$_SESSIONor mysqli is accepted and coerced, matching the server.5.5and'abc'are rejected. Build event and cart-item lists witharray_values()if you filtered them — the client re-indexes for you, but a gapped array would otherwise encode as a JSON object and the server rejects that.
Admin plane
Placement configuration. These require a service-actor token carrying
upsell.placements:read / :write; a user token gets a 403.
adminUpsertPlacement is a full replace, not a patch — channel, key,
touchpoint, enabled and display are required on every call, so a partial
object is a 400 rather than a merge. Only strategy, strategyParams,
display.maxItems and display.layout have server-side defaults. Scope is
derived from what you send: no parentId is the platform default, parentId
alone is a brand rule, parentId + businessId is one outlet.
Deleting without a parentId targets the platform default, removing the
placement for every tenant that has not overridden it, and needs
'confirmDeleteDefault' => true. This client only sends that flag when you
pass it truthy, so forgetting parentId gets you a 400 rather than a silent
wipe.
Non-2xx responses throw UpsellApiException (getStatus(), getBody()).
Wallet + credit line
getWallet(...) is a lightweight handle — no I/O. The wallet resolves
server-side on the first operation.
Which billing model? — start here
Prepaid wallets and merchant credit lines coexist, and which one a merchant is on decides what to render and which calls are legal.
balanceis on both arms and means the same thing either way: what the merchant can still spend.- One request for a prepaid merchant, two for a credit one, and never a
404 on the happy path — it reads
getBalance()['source']rather than callinggetCreditLine()and catching its 404, which would make the normal state of a prepaid merchant an error on every page load. - A wallet service older than the credit line (no
source) reads as prepaid rather than throwing. - The credit arm needs
wallet.credit:readon top ofwallet.balance:read. A token with only the latter gets a 403 from the second call — not a fallback toprepaid, which would render the frozenwallet_balancethese merchants no longer spend from. listCreditEvents()rows arelimit_changed,settledorsettlement_reversed; a reversal row carriescycleand both settlement kinds carryreleasedNetMinor/releasedGstMinor.- Not a guaranteed 200:
wallet_not_found(404) andunresolvable_wallet(422) both propagate, because both mean there is nothing to render.
Credit line
The ceiling is per merchant, not per wallet, so an outlet's id and its parent's id return the same line.
- Charges on a credit-line merchant must carry
breakup— consumption is tracked net of GST, so a missing split is400 credit_breakup_requiredrather than a guessed rate.allowNegativeis refused (400 credit_override_refused): it disables the balance guard for migration paths, and on a credit line it would disable the limit. - The merchant pays gross; settlement releases net. A ₹5,900 invoice on ₹5,000 of consumption frees ₹5,000. Passing gross hands back 18% too much — and it looks right in a test, since both figures sit on the same invoice row.
- Setting a limit never touches consumption, so raising one hands back exactly the difference. Lowering below what is consumed blocks charges immediately.
'enabled' => falseis refused while the merchant owes —409 credit_line_has_outstanding; readconsumedMinorfromcreditFigures()to say how much to settle. Disabling moves them back to a wallet balance, which with a balance owed abandons it. To stop a merchant trading use'limitMinor' => 0; to move them off, settle first.- Reusing an idempotency key with different figures is
409 credit_idempotency_conflict, not a silent replay. To correct a settled amount, reverse it and settle again rather than inventing a new ref — a new ref leaves the original settlement standing. - A plain
credit()is refused —400 credit_top_up_refused. These merchants have no balance to top up:wallet_balanceis the fiction the line replaces and is frozen, so the write would move nothing while answering 201. Credit comes back throughsettleInvoice. A genuine refund is accepted — sendreversalOforisRefundand it releases what the original charge consumed. getCreditUtilisationhangs off$platform->wallet, not a wallet handle: it is the one wallet call not scoped to a business.
Refusals worth branching on
WalletApiException::errorCode() gives the machine-readable code;
creditFigures() gives the credit-line figures on
credit_limit_reached.
credit_line_not_found (404) is a prepaid merchant, not a failure — use
getOverview() and you never see it.
Money
Every figure is ['amount' => float, 'amountMinor' => int]. Compute in
amountMinor (integer paise); amount is for display. On a ledger row
amount is gross and breakup.subTotalMinor is what consumed the
ceiling — creditMerchantId present means that row's
balanceBefore/balanceAfter are available credit, not a wallet
balance.
Vault (credentials and KYC documents)
$platform->vault stores per-brand payment-gateway / POS credentials and KYC
documents encrypted, and hands plaintext back only to tokens that carry a
vault.<class>:decrypt scope. Vault accepts only vault tokens, each bound
to one tenant: a brand (parentId) or one of its outlets (parentId +
businessId). A brand token reaches the brand's items and every outlet's; an
outlet token reaches only that outlet.
Backend: client credentials
The SDK mints a vault token for that tenant
(urn:uengage:params:oauth:grant-type:vault-business, every vault scope your
client's registry entry allows), caches it in the configured token cache until
60 s before expiry, and re-mints once on a 401. Your client needs
vault.tokens:issue plus the vault scopes in the auth registry.
List filters: type or class (one is required), tags (sent as
tag.<key>=<value>), status, businessId, includeOutlets (brand token
only), limit (≤ 100), cursor. list() returns one page
(['items' => ..., 'nextCursor' => ...]); listAll() is a generator that
follows nextCursor.
Decrypt and SecretValue
Every decrypted field is a Uengage\PlatformSdk\Vault\SecretValue. Only
->reveal() returns the plaintext; (string), string interpolation,
json_encode, print_r and var_dump all show [REDACTED], the plaintext
is not held in an object property (so var_export and (array) casts do not
show it), and serialize() / clone are refused. Call reveal() at the point
of use and do not store, log or cache the result. Only the active version of an
active credential is ever decrypted.
Frontend handoff: mintVaultToken
A backend mints a narrow, short-lived vault token and hands it to a frontend
or another service, the same way as mintUpsellToken / mintRealtimeToken:
Never put a vault.<class>:decrypt scope in a token bound for a browser; the
dashboard backend's client should not be granted it at all. The client needs
vault.tokens:issue; tokens are never cached. Bad ids, non-vault scopes, an
out-of-range expiresIn or an invalid onBehalfOf (1-128 printable ASCII
characters) throw InvalidArgumentException before sending; a refused mint or a
response that does not match the request throws AuthApiException.
Frontend handoff: mintAlertToken
A backend mints a short-lived alerts token for one dashboard user and hands it
to that user's frontend. The token only reaches alerts whose userIds include
that user (see the alerts audience rules): list is limited to them, and get /
setStatus answer 404 for anyone else's. Status changes made with it are
recorded as that user, so the frontend sends no actor.
The frontend uses the TypeScript SDK's browser-safe subpath:
The issuing client needs alerts.tokens:issue plus each alerts:* scope it puts
in a token; tokens are never cached. A bad userId, a non-alerts scope, an
out-of-range expiresIn or an invalid onBehalfOf throw
InvalidArgumentException before sending; a refused mint or a response that
does not match the request throws AuthApiException.
Static-token mode
A process that was handed a vault token uses it directly; the tenant comes from the token:
getToken is called when the token has expired and once after a 401 (the
request is then retried once). Without it, a 401 is final.
Documents (KYC)
upload() is one call: it registers the document (POST /documents, which
returns a presigned S3 POST), sends the file straight to S3 (no bearer), then
calls POST /documents/{id}/complete, which checks the object's size and
content type (409 upload_not_found / upload_mismatch). view() works only
for uploaded documents; a pending one answers 409 document_not_uploaded.
getByReference, list, listAll and setTags work as for credentials; the
list status filter takes pending or uploaded.
Types
With client credentials, $platform->vault->types() uses the ordinary service
token (it needs vault.types:read / vault.types:write); a tenant handle's
types() uses its vault token.
Errors
| Exception | When |
|---|---|
VaultDeniedException |
403 - every denial is {"error":"forbidden"}; the reason is audited |
VaultNotFoundException |
404 |
VaultConflictException |
409 - reference_conflict, version_conflict, credential_disabled, document_not_uploaded, upload_not_found, upload_mismatch |
VaultValidationException |
400 / 422 - bad input, unknown_type |
VaultUnavailableException |
5xx, or no response at all (status 0) |
All extend VaultApiException (getStatus(), getBody(), getErrorCode()),
which extends ApiException. Token-mint failures throw
AuthenticationException; bad local input (ids, expiresIn, scopes) throws
ConfigException before any request.
Token caching
By default Client::create() picks the best available cache:
- APCu (
Uengage\PlatformSdk\Token\ApcuTokenCache) - if ext-apcu is loaded and enabled. Shared across PHP-FPM workers on the host; recommended for production. - File (
Uengage\PlatformSdk\Token\FileTokenCache) - falls back tosys_get_temp_dir()/uengage-platform-sdk-php/. Atomic writes, 0600 permissions. Works everywhere.
Plug a custom backend (Redis, Memcached, your app's cache pool) by
implementing TokenCacheInterface and passing 'cache' => $yours to
Client::create().
For one-off scripts or tests where multi-request reuse doesn't
matter, use InMemoryTokenCache.
Error handling
The SDK throws typed exceptions:
| Exception | When |
|---|---|
Uengage\PlatformSdk\Exceptions\ConfigException |
bad client construction (multiple auth modes, etc) |
Uengage\PlatformSdk\Exceptions\AuthenticationException |
token mint rejected by auth surface |
Uengage\PlatformSdk\Zones\ZonesApiException |
non-2xx from /v1/zones/* |
Uengage\PlatformSdk\Wallet\WalletApiException |
non-2xx from /v1/wallet/* (->errorCode(), ->balanceMinor(), ->creditFigures()) |
Uengage\PlatformSdk\Business\BusinessApiException |
non-2xx from /v1/businesses/* |
Uengage\PlatformSdk\Audit\AuditApiException |
non-2xx from /v1/audit/events |
Uengage\PlatformSdk\Auth\AuthApiException |
non-2xx from /auth/business/* |
Uengage\PlatformSdk\Vault\VaultApiException (+ subclasses) |
non-2xx from /v1/vault/* (->getErrorCode()) |
Uengage\PlatformSdk\Alerts\AlertsApiException |
non-2xx from /v1/alerts/* |
InvalidArgumentException |
bad local input (non-uuid, out-of-range lat/lng, etc) |
All *ApiException types extend ApiException and expose
getStatus(): int + getBody(): string.
The SDK transparently rotates the token + retries once on a 401 when the auth mode supports invalidation, so most expiry-related 401s never surface to your code.
Testing the SDK locally
API surface — full reference
See /v1/zones/openapi.json, /v1/businesses/openapi.json,
/v1/audit/openapi.json, /v1/upsell/openapi.json, /v1/alerts/openapi.json,
/auth/business/openapi.json for the wire contracts. The PHP namespace structure mirrors the JS SDK 1:1 — refer
to packages/platform-sdk/src/<namespace>/ for the canonical type
shapes.
All versions of php-platform-sdk with dependencies
ext-curl Version *
ext-json Version *