Video chapters
- AppEngine Integrations and Webhooks
- Start with your organization and staff session
- Your application, your provider connection
- Find the maintained provider contract
- Discover the provider-specific contract
- Test the selected provider connection
- Two directions, different credentials
- A payment and its business result
- Create the unpaid order
- Confirm the genuine sandbox payment
- Find the matching provider event
- Apply the provider event and read the result
- One event, one business effect
- Repeat delivery without duplicating payment
- Refuse an unknown event
- Register a real TEST webhook endpoint
- Prepare the new automatic-delivery order
- Enable the registered receiver
- Receive the automatic signed callback
- Retire the course endpoint
- Verify signed bytes and replace the course secret
- Recover the missing event, preserve the result
- Match the callback to the business event
- Provider event to saved AppEngine result
Written guide
Why this course
Your business already has accounts with other services — Stripe for card payments, Twilio for texts, EasyPost for labels. You want them working inside your application without writing a Stripe client, a Twilio client and an EasyPost client, storing each business's keys yourself, and building an endpoint that listens for "payment succeeded" and finds the right order. AppEngine already has the clients (it calls them providers), stores each business's credentials as a saved connection, and has the listening endpoints (webhooks) that receive the provider's news and update the right record. You save the connection and call it; you don't build it.
The running example: connect a business's own Stripe account, prove it works, and have orders marked paid automatically when Stripe confirms the payment.
1. What is built in
Built-in data types
A data type (or collection) is a kind of record AppEngine already knows, with its fields defined in the SDK. A record is one saved item; its sk is its unique ID and data holds its fields.
| Data type | What it holds | Fields you will use |
|---|---|---|
config | one saved connection to a provider, with its credentials | name, provider (e.g. StripeProvider), type (e.g. Payment), useCases, priority (0–100), status (active, inactive, deprecated, use-platform), plus the provider's own fields — for Stripe publishableKey and secretKey |
sf_order | a placed order | number, status, amount, paymentRef, paymentGateway, paidAt |
sf_transaction | one payment against an order | ref, gateway, amount, status |
log | log entries; each Stripe event AppEngine has applied is kept as one | kind (stripe-webhook-event), eventId, eventType, objectId, processedAt |
Built-in providers
Discover installed providers through the current integration-types endpoint. Available providers and operations can change between deployments. Examples by area:
| Area | Providers (the name you call them by) |
|---|---|
| Payments | StripeProvider, PayPalProvider, HelcimProvider |
| Text, voice, email | TwilioProvider, MailgunProvider, SendGridProvider, ResendProvider, SMTPProvider |
| Shipping | EasyPostProvider, ShippoProvider, ShipEngineProvider, UPSProvider, FedExProvider |
| Social and ads | FacebookProvider, GoogleProvider, LinkedinProvider, TiktokProvider, PinterestProvider |
| Marketplaces | ShopifyProvider, WooCommerceProvider, EtsyProvider, EbayProvider, AmazonProvider |
Each provider declares its operations — the things it can do. Stripe's include test, paymentIntent, capturePayment, refundPayment, listTransactions, listEvents, setupWebhook, listWebhooks, createCheckoutSession and chargeOffSession.
Built-in actions
| You call | AppEngine does for you |
|---|---|
GET upstream/integration-types/:provider | describes a provider: its configuration fields, its operations, its type and use cases |
GET upstream/integration-use-cases/:useCase | lists the providers that serve a use case, such as Payment or Shipping |
POST upstream/save-integration | saves or updates a connection; inspect provider setup results and independently verify webhook registration |
POST upstream/call/:provider/:operation | loads the business's saved connection, starts the provider with its credentials and runs the operation; with no connection of its own, the business can use the platform's |
POST connect/webhook/:vendor?orgid=… | dispatches an event to the vendor-specific receiver for the selected business; authenticity checks, business effects and acknowledgements depend on that receiver's contract |
Webhook receivers are built in for stripe, paypal, helcim, plaid, twilio, sendgrid, mailgun, ses, easypost, facebook, instagram, whatsapp, google, youtube, twitter, linkedin, tiktok, pinterest, threads, mastodon and figma.
When a Stripe event arrives, AppEngine:
| Stripe event | What AppEngine does |
|---|---|
payment_intent.succeeded, checkout.session.completed | finds the order (or invoice, rental, delivery job) the payment belongs to; marks an order paid (or paid-partial) with paidAt, paymentRef and paymentGateway; marks the sf_transaction paid or creates it |
payment_intent.payment_failed | sets the order to payment_failed with paymentError, unless it was already paid |
charge.refunded, charge.refund.updated | records the refund |
customer.subscription.*, invoice.paid, invoice.payment_failed | announces the event to the rest of the platform |
| the same event twice | applies it once, answers duplicate: true the second time |
Which to use
- Use the built-in provider and webhook when one exists for the service. Calling Stripe yourself and writing the order status yourself skips the duplicate check, the order matching and the transaction record.
- Use the built-in data types: the connection is a
configrecord, the result lands onsf_order/sf_transaction. - Write your own only for a service AppEngine has no provider for — a small adapter on your server (Custom API gateway), or a new provider inside AppEngine if you run your own deployment (section 9).
2. Set up
Start at Appmint Studio and select your development organization. Open the AppEngine API reference for your assigned deployment. The Studio website address is not automatically the API origin.
For a first client connection, follow the staff/customer identity chapter, then the sign-in chapter. Complete any returned sign-in challenge before making administrative requests. The reference-navigation course shows how to find the current endpoint and caller contract. Keep your organization's provider secrets in its server-side configuration, not in a mobile or public browser bundle.
A staff session. Each request carries orgid — your organization ID, the business whose data you're working in — and Authorization: Bearer <token>, the signed-in staff member's bearer token. API foundations shows how to get both.
export API="https://your-appengine-host"
export ORG="your-organization-id"
export STAFF_TOKEN="your-staff-access-token"Stripe test keys for the business's own Stripe account: a secret key (sk_test_…) and its publishable key (pk_test_…) from the Stripe dashboard. Use test mode while you learn.
A public address for the server. Stripe has to reach AppEngine. The webhook address is built from the server's APPENGINE_URL (or BASE_URL) setting, so that must be the public HTTPS address — see Platform deployment.
Run the companion
Read the companion README. With Node 20 or newer, set APPENGINE_API_ORIGIN to your assigned backend and APPENGINE_ORG to your development organization, enable only the documented sandbox confirmation route, then run npm start. Open the local development app and connect your staff account. It is labelled AppEngine developer companion. Use an existing training customer, the workshop-guide fixture and your own Stripe test connection; the example refuses live keys and totals over $50.
3. Look up the provider
curl -sS "$API/upstream/integration-types/StripeProvider" \
-H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"{
"name": "StripeProvider",
"type": "Payment",
"useCases": ["Payment", "Subscription"],
"configSchema": { "type": "object", "properties": {
"publishableKey": { "type": "string" }, "secretKey": { "type": "string" },
"webhookId": { "type": "string", "readOnly": true }, "webhookSecret": { "type": "string", "readOnly": true }, "webhookUrl": { "type": "string", "readOnly": true } } },
"operation": ["paymentIntent", "capturePayment", "test", "listEvents", "setupWebhook", "listWebhooks", "…"]
}
configSchema tells you which fields the connection needs; operation lists what you can call. Leave the provider name off (GET upstream/integration-types) to list every provider.
4. Save the connection
The connection is a config record. Put the provider's fields straight into data:
curl -sS -X POST "$API/upstream/save-integration" \
-H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" \
-H 'Content-Type: application/json' \
--data '{
"datatype": "config", "isNew": true,
"data": { "name": "stripe-main", "provider": "StripeProvider", "type": "Payment",
"status": "active", "priority": 1,
"publishableKey": "pk_test_…", "secretKey": "sk_test_…" }
}'The response is the saved record. Keep its sk:
export CONFIG_ID="6706a0…"Keep one active Stripe connection per business. Payments use the one you name by configId; the webhook uses the business's active Stripe connection, so a second active one makes that choice unpredictable.
The response carries the secret key, so show only name, provider and status in any screen.
5. Prove it works
Call the test operation with your connection's ID. Operation arguments go in data:
curl -sS -X POST "$API/upstream/call/StripeProvider/test" \
-H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" \
-H 'Content-Type: application/json' \
--data "{\"configId\":\"$CONFIG_ID\",\"data\":{}}"{ "success": true, "livemode": false, "message": "Connected to Stripe (test mode)" }Stripe's test reads the account balance — nothing is charged. livemode: false confirms you're on the test keys. A wrong key comes back as an error with Stripe's reason.

Every other operation is called the same way: POST upstream/call/StripeProvider/<operation> with {"configId": "...", "data": { ...arguments }}. Leave out configId and AppEngine uses the business's active Stripe connection.
6. Let Stripe tell AppEngine when a payment lands
Do not infer registration from a successful connection save. The current provider exposes setupWebhook, listWebhooks and testWebhook. Inspect the actual saved registration and provider-side result. The inspected save path calls a post-save hook but does not establish that its returned webhook metadata was persisted; a separate setup/readback check is required. Check the registered endpoints from Stripe's side:
curl -sS -X POST "$API/upstream/call/StripeProvider/listWebhooks" \
-H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" \
-H 'Content-Type: application/json' --data "{\"configId\":\"$CONFIG_ID\",\"data\":{}}"{ "success": true, "webhooks": [ { "id": "we_1Q…", "url": "https://your-appengine-host/connect/webhook/stripe?orgid=your-organization-id",
"status": "enabled", "enabledEvents": ["payment_intent.succeeded", "checkout.session.completed", "charge.refunded", "…"], "created": 1760275200 } ] }The address is connect/webhook/stripe with your organization ID in orgid, which is how AppEngine knows whose event it is. Stripe sends payment, refund, subscription and invoice events there.
If the list is empty, or the URL isn't your public address (the server's APPENGINE_URL was wrong when you saved), fix the setting and register it again:
curl -sS -X POST "$API/upstream/call/StripeProvider/setupWebhook" \
-H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" \
-H 'Content-Type: application/json' \
--data "{\"configId\":\"$CONFIG_ID\",\"data\":{}}"{ "success": true, "webhookId": "we_1Q…", "webhookUrl": "https://your-appengine-host/connect/webhook/stripe?orgid=your-organization-id",
"message": "Webhook created successfully" }Running setup again can update an existing endpoint at that address. Check endpoint ownership first: do not use forceRecreate or change event subscriptions on an endpoint shared with another application. Verify the returned ID, URL and signing configuration after setup; a successful response is not proof that a public provider delivery reached the business record.
When this organization's Stripe configuration has a webhookSecret, AppEngine verifies Stripe-Signature against the original raw request bytes. Keep the endpoint's signing secret on the server, distinct from the Stripe API secret key. A parsed-and-re-serialized body is not a substitute for the signed bytes. When no webhookSecret is configured, the current receiver instead retrieves Stripe's authoritative event by ID using the organization's Stripe key and acts on that returned event; an invented event ID is refused. Stripe v2 thin events also use authoritative event retrieval. These are current Stripe-specific branches, not a promise that every other provider uses the same check.
Recorded setup: dedicated TEST receiver
The film uses a separate restricted HTTPS receiver so registration can be demonstrated without reconfiguring existing endpoints. This is a developer companion, not Appmint Studio. Its source is webhooks/automatic-delivery.mjs in the companion. Follow the automatic-delivery setup for the staff session, TEST connection, controlled customer, $12 product and Cloudflare client prerequisites.
- Open the companion and choose Register course endpoint. It creates a new Stripe TEST endpoint subscribed only to
payment_intent.succeeded, disables it initially, then saves the returned endpoint ID, URL and signing secret in a dedicated inactive AppEngine connection. The secret stays server-side. - Choose Prepare $12 order. Confirm the saved order is
newand has no payment transaction before enabling delivery. - Choose Enable course receiver. The harness selects its course connection and enables only its owned endpoint. The current AppEngine receiver chooses the organization's active Stripe configuration, so this test requires an isolated organization with no competing payment work.

The public listener accepts only its random callback path, verifies the exact signed bytes and TEST event/payment identity, and forwards to a fixed training organization. It is not a general backend proxy. This custom-address example registers through Stripe's API; the ordinary built-in setupWebhook route uses your deployment's configured address. Never change a shared deployment URL just to recreate this temporary lesson.
7. Take a payment and watch the order change
Take a test payment for an order the normal way — through the checkout in Headless storefront or a payment in Finance APIs. Pay with Stripe's test card 4242 4242 4242 4242.
When Stripe confirms, it posts payment_intent.succeeded to the registered webhook. AppEngine verifies the signature when a signing secret is configured, or retrieves the authoritative event in the no-secret branch described above. It finds the order from the payment reference or its explicit orderNumber metadata, then answers Stripe:
{ "received": true, "eventType": "payment_intent.succeeded" }Read the order:
curl -sS -X POST "$API/repository/find/sf_order" \
-H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" \
-H 'Content-Type: application/json' \
--data '{"query":{"data.paymentRef":"pi_3Q…"},"options":{"page":1,"pageSize":1}}'Its status is paid, paidAt is set, paymentGateway is stripe, and the matching sf_transaction is paid. An order that had already moved on (shipped, refunded, cancelled) is left as it is.
And the record of the event itself:
curl -sS -X POST "$API/repository/find/log" \
-H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" \
-H 'Content-Type: application/json' \
--data '{"query":{"data.kind":"stripe-webhook-event"},"options":{"page":1,"pageSize":5}}'Each entry has the Stripe eventId, eventType, the paid object's objectId and processedAt.
Recorded automatic delivery and cleanup
Choose Confirm TEST payment. Stripe confirms the fresh sandbox PaymentIntent and independently sends its signed event to the registered receiver. No replay button is used here. AppEngine returns200; order F0BD0LKUH becomes paid and one $12 transaction is saved. Reloading shows the same transaction ID. A provider success and a saved business result are separate checks, even when they happen close together.

The harness pauses its endpoint and restores the original AppEngine connection after the result. Choose Delete course endpoint to remove only the temporary provider registration. The saved order/payment remain. The captured cleanup compared the pre-existing provider endpoint objects and original configuration: both were unchanged. Stop the harness to close its tunnel and receiver. The companion README explains private recovery state if a process is interrupted.

Follow the recorded recovery example in the companion
The companion is a developer example, not Appmint Studio. Start it using its README, connect the staff account and select the existing Stripe TEST connection. The server keeps provider keys out of the browser. Follow these steps in order; each readback uses the same saved order.
- Enter your controlled customer email and choose Create $12 test order. The server quotes the workshop guide and creates the order using the storefront action. Confirm the status is new, with no payment transaction.
- Choose Confirm sandbox payment. The PaymentIntent is created through AppEngine’s built-in provider and confirmed with Stripe’s documented test payment method. Stripe confirms the payment, while the order can still remain new until its event is applied.
- Choose Load recent succeeded events. Select only the event matching both the order number and PaymentIntent. For a busy account, continue with the returned cursor rather than assuming the first page contains your event.
- Choose Replay selected course event. In this recording the existing training configuration has no signing secret, so AppEngine retrieves the genuine event from Stripe before applying it. This is deliberate recovery through the current retrieval branch, not a recording of autonomous public delivery.
- Read the order and transaction together. Recorded order SDMJUAEG5 changed from new to paid, retained its $12 USD amount, received the provider reference and paid timestamp, and gained one matching paid payment transaction plus a processed-event entry.




Send the same event again
Providers retry, so the same event can arrive twice. With a configured signing secret, use Resend on the event in Stripe Workbench; this creates a fresh signed delivery. Posting just an event ID is supported only by the current no-secret retrieval branch. The following example is that deliberate recovery route; do not remove a configured secret to make it work.
curl -sS -X POST "$API/connect/webhook/stripe?orgid=$ORG" \
-H 'Content-Type: application/json' --data '{"id":"evt_3Q…"}'{ "received": true, "duplicate": true, "eventType": "payment_intent.succeeded" }
The order is untouched. AppEngine answers:
| Status | When | What the provider does |
|---|---|---|
| 200 | applied, a duplicate, or a type AppEngine doesn't act on | stops |
| 400 | event cannot be authenticated or the receiver/configuration is wrong | failed delivery; inspect and correct the cause rather than assuming retries stop |
| 500 | AppEngine couldn't apply it right now | retries later |

8. Catch up, change and retire
Catch up on events. If the server was down for a while, list recent events from Stripe and compare with the log entries:
curl -sS -X POST "$API/upstream/call/StripeProvider/listEvents" \
-H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" \
-H 'Content-Type: application/json' \
--data "{\"configId\":\"$CONFIG_ID\",\"data\":{\"limit\":20,\"types\":[\"payment_intent.succeeded\"]}}"{ "data": [ { "id": "evt_3Q…", "type": "payment_intent.succeeded", "created": 1760275301, "livemode": false, "data": { "object": { "id": "pi_3Q…" } } } ],
"hasMore": false }data also takes createdGte (Unix seconds) and startingAfter (the last event ID of the previous page). Continue while hasMore is true and carry the final returned ID into the next page. Match events by ID and payment object, not timestamp alone. For a missing processed-event entry, request a fresh signed provider resend when signing is enabled. The ID-only recovery call above applies to the no-secret retrieval branch only. Re-read the exact order and transaction after either route.
Stripe does not guarantee event order, and a successful manual resend does not cancel already scheduled automatic retries. Duplicate handling still matters. Current delivery status and the next retry appear in Workbench → Webhooks → the destination → Event deliveries. See Stripe delivery behavior.
Rotate a key. Read the saved record, change the key, and save it again with its sk — that updates it in place:
curl -sS "$API/repository/get/config/$CONFIG_ID" -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" > config.json
# edit data.secretKey in config.json, keep sk and the other fields
curl -sS -X POST "$API/upstream/save-integration" \
-H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" \
-H 'Content-Type: application/json' --data-binary @config.jsonObtain the replacement credential from the provider first; changing a field in AppEngine does not create or revoke a Stripe API key. Save only the intended connection, run test, then perform one controlled read/operation through it. Retire the old credential at the provider only after the replacement works. Keep API-key rotation separate from the endpoint signing secret. For a signing-secret roll, update AppEngine during the provider's overlap window and test a fresh delivery; this AppEngine configuration holds one active signing-secret value, so do not promise that it independently maintains a multi-secret grace period.
Switch it off or hand over. status decides what happens:
status | Effect |
|---|---|
active | this connection is used |
inactive, deprecated | switched off; calls fail rather than quietly using another account |
use-platform | keep the record, but use the platform's own Stripe account |
Remove it. First stop new work and resolve outstanding events. Disable this dedicated connection and confirm calls fail. Remove only its owned endpoint in Stripe Workbench → Webhooks, then delete its AppEngine record when retention requirements allow. Deleting a connection can change fallback selection, so do not use deletion as a substitute for an explicit inactive state:
curl -sS -X DELETE "$API/repository/delete/config/$CONFIG_ID" \
-H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"Other providers
Provider discovery, saved configuration and operation calls share a common entry point. Callback routes and contracts do not: some providers use challenge verification, separate status/voice/message routes, or additional authoritative lookups. Inspect the maintained provider reference and linked domain course rather than constructing a callback URL from its name. Accounts that connect by the owner signing in — Google, Facebook, Instagram, marketplaces — are connected on screen in Configuration → Integration Config or CRM → Social Media; see Integrations and Social media. Once connected, they're config records you call the same way. The scheduled imports that keep their data fresh are in Scheduled jobs and sync.
9. A service without a built-in provider
Use Custom API Gateway for your server boundary and External API Integration and Data Mapping for an adapter, transformation and error recovery. Those courses carry the implementation; this lesson stays on AppEngine's provider connection and callback lifecycle. A self-hosted provider extension also needs its own event receiver and authenticates provider events before changing business data.
Recorded signature and connection-lifecycle checks
The local signing exercise uses a generated course-only secret with the actual AppEngine receiver and an already processed genuine TEST event. Run local signing checks sends the exact bytes with a matching signature (200 duplicate), changes one byte with the original signature (400), and uses an incorrect secret (400). Every response is followed by exact order/transaction readback.
After replacing only the course connection's local signing secret, a request signed with the former secret returns400; the replacement succeeds as a duplicate. The inactive explicitly selected course connection also refuses a provider operation with404. The original organization connection is restored after the bounded exercise. This demonstrates configuration replacement and refusal, not provider API-key issuance or rolling an existing Stripe endpoint secret.

Use javascript/signed-probe.mjs or python/signed_probe.py for byte-preserving local probes in your own isolated fixture. An old timestamped request is not a fresh provider resend. For a real provider-issued replacement, use the provider's key/secret management, save the intended AppEngine config, then verify a fresh provider delivery. Never remove the signing secret to bypass a failed check.
Recording and delivery status
The actual browser walkthroughs now cover provider discovery/test, genuine TEST payment, authoritative event replay, duplicate/unknown-event results, dedicated public registration, autonomous signed delivery, retirement, local signature refusal and course-secret replacement. The illustrative chapters explain their distinct responsibilities. The 7:21 video is ready for review: 24 chapters at 1440p30. Independent encoded audio and graphical checks passed, including the corrected webhook graphic, and all chapter links were verified. Watch the course.
Maintained example source
The browser client, JavaScript, TypeScript, Python, cURL, Postman and dedicated signed-webhook receiver are published in appmint-examples. The downloadable ZIP is a recording snapshot; use the maintained repository for subsequent updates.