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:
json
{
"scopes": [],
"webhooks": [],
"api_permissions": [
"payment_sessions:write"
],
"supported_currencies": [
"USD",
"EUR",
"GBP",
"AUD",
"CAD"
],
"endpoints": {
"payment_session": "https://stripe-app.example.com/sessions",
"refund": "https://stripe-app.example.com/refunds"
},
"capabilities": {
"refunds": true
},
"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."
}
]
}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://api.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://api.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.
Capabilities
Declare what your provider can do under the capabilities key in your manifest. All flags default to false; omit any you don't support.
| Flag | Meaning |
|---|---|
saved_payment_methods | Your server can store a buyer's payment method and reuse it for future sessions without re-prompting. |
off_session_charges | Your server can initiate a charge without the buyer being present (requires saved_payment_methods). |
refunds | Your server handles refund requests POSTed to your refund endpoint. |
manual_capture | Your server uses an authorize-then-capture flow; the platform may ask you to capture or void separately. |
offline | Your server collects payment offline (e.g. cash on arrival). The platform keeps the booking pending until the host records receipt rather than waiting for a session resolve. |
Degrade gracefully. Schedules never hard-depend on any flag; capabilities only change which collection strategy the platform uses. An offline-capable provider, for example, keeps the booking in a pending state until the owner confirms receipt; no session needs to be resolved programmatically.
The reference Stripe app declares refunds: true.
Refunds
If your manifest declares refunds: true in capabilities, Stayblox calls your refund endpoint (platform → app) when a host refunds a resolved session. Reverse the charge with your provider and return 200.
Payment schedules
Bookings may carry owner-configured installment schedules. When they do, a payment session is linked to a specific installment rather than the full booking total, so amount in the session payload can be less than the booking's total value. This was always technically possible, but with schedules it is the norm rather than the exception.
The wire protocol is unchanged: same signed session POST payload, same GraphQL mutations (paymentSessionResolve, paymentSessionReject, paymentSessionPending). Treat amount as the authoritative charge amount for that session and don't try to reconcile it against the booking total yourself.
Checklist
- [ ] Manifest registered with
type: payment,endpoints,settings_schema. - [ ]
capabilitiesblock declares the flags your server actually supports. - [ ]
payment_sessionendpoint verifies the signature and returns{ redirect_url }. - [ ] Provider callback resolves/rejects the session over GraphQL.
- [ ]
userErrorschecked on every mutation. - [ ] Resolve handled idempotently.