Appearance
Payment apps
A payment app lets hosts charge buyers at checkout through your provider. It implements the Stayblox payment-session protocol: Stayblox opens a session against your server, the buyer pays on your hosted page, and you report the outcome back over GraphQL.
This guide takes you from manifest to a resolved session. For a complete runnable server, see the reference app.
1. Declare your app (manifest)
A payment app is registered as a marketplace App with type: "payment" and a manifest. Here is the reference Stripe app's manifest (from StripePaymentAppSeeder):
json
{
"endpoints": {
"payment_session": "https://stripe-app.example.com/sessions",
"refund": "https://stripe-app.example.com/refunds"
},
"capabilities": {
"supports_refund": true,
"supports_capture": false,
"supports_void": false,
"supports_3ds": true
},
"supported_currencies": [
"USD",
"EUR",
"GBP",
"AUD",
"CAD"
],
"settings_schema": [
{
"key": "stripe_account",
"type": "text",
"label": "Stripe account ID",
"placeholder": "acct_XXXXXXXX",
"required": true,
"help": "Your connected Stripe account, used by the provider to route charges. No secret keys are stored here."
},
{
"key": "statement_descriptor",
"type": "text",
"label": "Statement descriptor",
"required": false,
"help": "Shown on the buyer's card statement."
}
],
"webhooks": [],
"api_permissions": [
"payment_sessions:write"
]
}settings_schema fields
These are the fields the host fills in when configuring the install. Values are passed to your server in the session payload's settings.
| Key | Type | Label | Required | Help |
|---|---|---|---|---|
stripe_account | text | Stripe account ID | yes | Your connected Stripe account, used by the provider to route charges. No secret keys are stored here. |
statement_descriptor | text | Statement descriptor | no | Shown on the buyer's card statement. |
Supported currencies
USD, EUR, GBP, AUD, CAD
An empty list means the provider accepts any currency.
Endpoints
| Key | Purpose |
|---|---|
payment_session | Stayblox POSTs here to open a session; returns { redirect_url }. |
refund | Stayblox POSTs here to reverse a resolved session. |
2. Open a session (platform → app)
When a buyer chooses your app at checkout, Stayblox creates a pending session and POSTs it to your manifest's payment_session endpoint. The request is HMAC-signed — verify it before acting (see Signing & security).
Stayblox POSTs this signed JSON to your app's payment_session endpoint (keys from RemotePaymentClient::sessionPayload()):
json
{
"session_id": "9b2c1f0e-4d6a-4f3b-8c21-7a0b5e9d1234",
"amount": 100.0,
"currency": "USD",
"payable_type": "App\\Models\\Booking",
"payable_id": 42,
"return_url": "https://shop.example/pay/return/9b2c1f0e",
"cancel_url": "https://shop.example/pay/cancel/9b2c1f0e",
"api_base_url": "https://app.stayblox.com/developer/api/2026-01/graphql",
"version": "…",
"settings": { "stripe_account": "acct_123" }
}| Field | Description |
|---|---|
session_id | The Stayblox payment-session id. Echo it back when you resolve/reject the session. |
amount | Amount to charge, in major currency units. |
currency | ISO 4217 currency code (uppercase). |
payable_type | The morph type of what is being paid for (e.g. a booking). |
payable_id | The id of the payable record. |
return_url | Send the buyer here after a successful payment. |
cancel_url | Send the buyer here if they abandon checkout. |
api_base_url | The GraphQL endpoint to call back to settle the session. |
version | |
settings | Non-secret host config from the install (the fields you declared in settings_schema). Secrets stay on your server. |
Respond 200 with { "redirect_url": "https://…" } — the hosted payment page Stayblox redirects the buyer to.
3. Report the outcome (app → platform)
After the buyer pays (or fails, or you're still waiting on an async result), call the Developer API with your bearer token to move the session to its final state.
The session lifecycle on the Stayblox side:
PENDING → PROCESSING → ┬─ resolve ─▶ RESOLVED (paid)
├─ reject ─▶ REJECTED (declined)
└─ pending ─▶ PENDING_EXTERNAL (awaiting async outcome)resolve, reject, and cancel/expire are terminal. A session may only be acted on by the app that owns it (resolved from your token).
Resolve a paid session
graphql
mutation Resolve($id: ID!, $ref: String) {
paymentSessionResolve(id: $id, providerReference: $ref) {
paymentSession { id status amount currency providerReference }
userErrors { field message }
}
}bash
curl -s https://app.stayblox.com/developer/api/2026-01/graphql \
-H "Authorization: Bearer $STAYBLOX_APP_TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"query": "mutation($id: ID!, $ref: String){ paymentSessionResolve(id:$id, providerReference:$ref){ paymentSession{ id status } userErrors{ message } } }",
"variables": { "id": "9b2c1f0e-...", "ref": "pi_3Q…" }
}'Resolving is idempotent — replaying the same resolve does not record a second payment, so it's safe to retry on provider webhook redelivery.
Reject a failed session
graphql
mutation Reject($id: ID!, $reason: String) {
paymentSessionReject(id: $id, reason: $reason) {
paymentSession { id status }
userErrors { field message }
}
}Mark a session pending (async outcomes)
For payment methods that settle asynchronously, acknowledge receipt and resolve later when the provider confirms:
graphql
mutation Pending($id: ID!, $ref: String) {
paymentSessionPending(id: $id, providerReference: $ref) {
paymentSession { id status }
userErrors { field message }
}
}4. Handle errors
Every mutation returns a userErrors array. On success it's empty; on a bad request it describes the problem (e.g. an unknown session id) without throwing a GraphQL error. Always check it before treating a call as successful.
Refunds
If your manifest declares supports_refund, Stayblox calls your refund endpoint (platform → app) when a host refunds a resolved session. Reverse the charge with your provider and return 200.
Checklist
- [ ] Manifest registered with
type: payment,endpoints,settings_schema. - [ ]
payment_sessionendpoint verifies the signature and returns{ redirect_url }. - [ ] Provider callback resolves/rejects the session over GraphQL.
- [ ]
userErrorschecked on every mutation. - [ ] Resolve handled idempotently.