The builder writes the app. The prompt writes the checkout.
AI builders are quick at screens and easy to get wrong at the part where money moves, because payment code has rules that look optional until they cost you. Each prompt below carries those rules, written for the tool you use, so what it generates starts from the right place.
Every prompt talks to the same PaymentHood API, so it works with whichever payment provider you have set up in the Console. Want the detail first? The full reference is at docs.paymenthood.com (opens in new tab).
Your path from idea to first payment
- Create a sandbox app in the PaymentHood Console and add a payment provider profile. Copy its App ID and API key.
- In the Console open Settings, then Webhook, and turn on webhook signing. Copy the whsec_ secret: it is shown once.
- Pick your tool below, copy its prompt and paste it into the project that sells something.
- When it asks, add the three secrets through its secure form. Never paste them into the chat.
- Pay with your provider's test card on the deployed site, and watch the order go from pending to paid.
- When that works, repeat with your live app's credentials.
Pick the tool you build with
Each prompt is complete on its own. Copy it, paste it into your project, and add the secrets when the tool asks for them.
Lovable
Watch for
- Supabase blocks calls without a login by default, so the webhook function needs JWT verification turned off. The prompt asks for this. Without it every webhook is a 401 and is retried.
- Test on the deployed app, not in the editor preview.
- Check that row level security is on for the orders table before real customers use it.
Platform notes follow Lovable's own documentation and Supabase's Edge Function guide. Read more: Lovable: payments (opens in new tab), Supabase: Edge Function auth (opens in new tab).
Add PaymentHood checkout to this app. PaymentHood is a hosted checkout: I create a payment on the server, send the customer to a PaymentHood page where they pay, and PaymentHood reports the result to my server by webhook. Card details never touch my app. Reference: https://docs.paymenthood.com/quickstart/
BUILD
1. create-checkout (server): for the signed-in user's order, read the price from my database (never from the browser), create the payment, and return the redirectUrl. The browser then redirects to it.
2. paymenthood-webhook (server, public): receives PaymentHood's notifications and updates the order.
3. A return page at /checkout/return. It shows "Confirming your payment" and re-reads the order from my own database until it is paid, failed, or about a minute has passed. It never marks anything paid itself.
4. An orders table: id (uuid), user id, amount, currency, status (pending, paid, failed, refunded), paymenthood_payment_id, updated_at.
PLATFORM NOTES (Lovable)
- Put both server functions in Supabase Edge Functions (supabase/functions/), written for Deno. Nothing that calls PaymentHood may run in the browser, and no secret may sit in a VITE_ variable or in any client file.
- Store the three secrets as Supabase Edge Function secrets, and read them with Deno.env.get. When you need a secret from me, ask for it through Lovable's secure secret form. I will not paste it into this chat.
- Supabase rejects any call without a valid JWT by default, and PaymentHood's webhook has none. Turn JWT verification off for the webhook function only, with verify_jwt = false for that function in supabase/config.toml, and rely on the signature check. Keep verification on for the function that creates the payment, and require the signed-in user there.
- In the webhook function read the raw body with await req.text() before anything else, verify the signature on that string, and only then call JSON.parse.
- Create an orders table with row level security on. A signed-in user can read only their own rows and can never write the status. Only the webhook function, using the service role key, changes the status.
- The webhook URL is the deployed function URL, https://<project-ref>.supabase.co/functions/v1/<webhook-function-name>. The editor preview is not reachable by PaymentHood, so test on the published app.
SECRETS
- Three server-only secrets: PAYMENTHOOD_APP_ID, PAYMENTHOOD_API_KEY, PAYMENTHOOD_WEBHOOK_SECRET. Never put them in frontend code, in a VITE_ or NEXT_PUBLIC_ variable, in the repository, or in a log line. Do not ask me to paste them into this chat: tell me which are missing and let me add them through the platform's secure secrets form.
- Where I get them: the App ID and API key are in the PaymentHood Console (https://console.paymenthood.com) for my app. The webhook secret comes from Settings, Webhook, Enable webhook signing. It starts with whsec_ and is shown only once.
- Sandbox and live are separate PaymentHood apps with separate credentials. Start with sandbox.
CREATE THE PAYMENT
POST https://api.paymenthood.com/api/apps/{APP_ID}/payments/hosted-page
Headers: Authorization: Bearer {API_KEY} and Content-Type: application/json
Send all of these every time, including the ones that look optional:
{
"referenceId": "<my order uuid, saved on the order before this call>",
"amount": 20.00,
"currency": "USD",
"returnUrl": "https://<my deployed domain>/checkout/return?order=<order id>",
"autoCapture": true,
"showPayRecurringInCheckout": false,
"registerAutoPayment": false,
"webhookUrl": "<my deployed paymenthood-webhook URL>",
"customerOrder": { "customer": { "customerId": "<my user id>" } }
}
- amount is a decimal in MAJOR units: 20.00 means twenty dollars. Do not convert to cents or multiply by 100, which is the Stripe habit and would charge 100 times too much.
- Success is 201 Created. Redirect the browser to the redirectUrl in the response.
- referenceId is the idempotency key and is unique per app. If the call answers 409, read the payment back (see the webhook steps) instead of creating a second one. If its paymentState is still Created and it has a redirectUrl, send the customer there.
WEBHOOK (do these steps in this order)
PaymentHood POSTs {"payment":{"paymentId":90210,"paymentState":"Captured","paymentStateId":7,"referenceId":"..."}} and nothing else, so the body is only a hint that something changed.
1. Read the raw body as text before any JSON parsing. A re-serialised body never verifies.
2. The header X-PaymentHood-Signature looks like t=1785000000,v1=<64 lowercase hex characters>. Parse it by key, not by position.
3. Reject if t is more than 300 seconds from the current Unix time.
4. expected = lowercase hex of HMAC-SHA256 with the key PAYMENTHOOD_WEBHOOK_SECRET over the string t + "." + rawBody.
5. Compare expected with v1 in constant time. Treat malformed hex as a failed check, never as an exception. On failure answer 400 and stop.
6. Only now parse the JSON.
7. GET https://api.paymenthood.com/api/apps/{APP_ID}/payments/referenceId:{referenceId} with the same bearer header, and set the order from paymentState in that response, never from the webhook body.
8. Answer 200 once the order is updated. Keep all of this to a few seconds, because serverless functions can be stopped the moment the response is sent. If the read-back or the update fails, answer 500 so PaymentHood retries.
The endpoint must not require a login, session or token from the caller. PaymentHood has none; the signature is the authentication.
ORDER STATES
- Mark an order paid only when the read-back paymentState is Captured. Failed means the order failed, and the reason is in lastError. Mark refunded only on Refunded.
- Webhooks are retried and can arrive out of order. The handler must be safe to run twice and must never move an order backwards, for example from paid to pending.
- A declined card is not an HTTP error: the API answers 200 with paymentState Failed. Branch on paymentState, not on the status code.
- The return URL is not proof of payment. The customer can close the tab, refresh it, or replay it.
- Refunds, only if I ask later: POST https://api.paymenthood.com/api/apps/{APP_ID}/payments/{paymentId}/refund with the numeric paymentId, not the referenceId, and wait for Refunded.
SELF-CHECK
With secret whsec_test_secret, t 1720000000 and body {"payment":{"paymentId":42,"paymentState":"Captured"}}, the correct v1 is e49595faf1e42b54d3634f76233d28a893e71dc8f1cfdf8c052a42004f0123fb. Use this as a test.
WHEN YOU ARE DONE
List the secrets I still need to add, and give me the one URL PaymentHood will call, so I can test on the deployed app.
Bolt.new
Watch for
- Server code has to live in a Supabase or Netlify function. Anything in the browser is readable.
- Add the secrets to the deployed environment as well as the preview.
- Test on the deployed site: a preview URL cannot receive webhooks.
Platform notes come from third-party guides and from how Supabase and Netlify work. Run the flow in sandbox before you go live.
Add PaymentHood checkout to this app. PaymentHood is a hosted checkout: I create a payment on the server, send the customer to a PaymentHood page where they pay, and PaymentHood reports the result to my server by webhook. Card details never touch my app. Reference: https://docs.paymenthood.com/quickstart/
BUILD
1. create-checkout (server): for the signed-in user's order, read the price from my database (never from the browser), create the payment, and return the redirectUrl. The browser then redirects to it.
2. paymenthood-webhook (server, public): receives PaymentHood's notifications and updates the order.
3. A return page at /checkout/return. It shows "Confirming your payment" and re-reads the order from my own database until it is paid, failed, or about a minute has passed. It never marks anything paid itself.
4. An orders table: id (uuid), user id, amount, currency, status (pending, paid, failed, refunded), paymenthood_payment_id, updated_at.
PLATFORM NOTES (Bolt)
- Bolt previews run in the browser, so anything in client code can be read by anyone. Put both server functions in Supabase Edge Functions or Netlify Functions, whichever this project deploys to, and keep the three secrets in that service's server-side secrets or environment settings. Not in client code and not in a file the browser can read.
- The deployed site needs its own copy of every secret. A variable that works in the preview but is missing on the deployed site is the usual reason server routes fail after deploying.
- If you use Supabase: it rejects calls without a valid JWT by default, so turn JWT verification off for the webhook function only and rely on the signature check. Also turn row level security on for the orders table, with users able to read only their own rows and never write status.
- The webhook URL must be the deployed function URL. A preview URL inside the editor is not reachable by PaymentHood.
SECRETS
- Three server-only secrets: PAYMENTHOOD_APP_ID, PAYMENTHOOD_API_KEY, PAYMENTHOOD_WEBHOOK_SECRET. Never put them in frontend code, in a VITE_ or NEXT_PUBLIC_ variable, in the repository, or in a log line. Do not ask me to paste them into this chat: tell me which are missing and let me add them through the platform's secure secrets form.
- Where I get them: the App ID and API key are in the PaymentHood Console (https://console.paymenthood.com) for my app. The webhook secret comes from Settings, Webhook, Enable webhook signing. It starts with whsec_ and is shown only once.
- Sandbox and live are separate PaymentHood apps with separate credentials. Start with sandbox.
CREATE THE PAYMENT
POST https://api.paymenthood.com/api/apps/{APP_ID}/payments/hosted-page
Headers: Authorization: Bearer {API_KEY} and Content-Type: application/json
Send all of these every time, including the ones that look optional:
{
"referenceId": "<my order uuid, saved on the order before this call>",
"amount": 20.00,
"currency": "USD",
"returnUrl": "https://<my deployed domain>/checkout/return?order=<order id>",
"autoCapture": true,
"showPayRecurringInCheckout": false,
"registerAutoPayment": false,
"webhookUrl": "<my deployed paymenthood-webhook URL>",
"customerOrder": { "customer": { "customerId": "<my user id>" } }
}
- amount is a decimal in MAJOR units: 20.00 means twenty dollars. Do not convert to cents or multiply by 100, which is the Stripe habit and would charge 100 times too much.
- Success is 201 Created. Redirect the browser to the redirectUrl in the response.
- referenceId is the idempotency key and is unique per app. If the call answers 409, read the payment back (see the webhook steps) instead of creating a second one. If its paymentState is still Created and it has a redirectUrl, send the customer there.
WEBHOOK (do these steps in this order)
PaymentHood POSTs {"payment":{"paymentId":90210,"paymentState":"Captured","paymentStateId":7,"referenceId":"..."}} and nothing else, so the body is only a hint that something changed.
1. Read the raw body as text before any JSON parsing. A re-serialised body never verifies.
2. The header X-PaymentHood-Signature looks like t=1785000000,v1=<64 lowercase hex characters>. Parse it by key, not by position.
3. Reject if t is more than 300 seconds from the current Unix time.
4. expected = lowercase hex of HMAC-SHA256 with the key PAYMENTHOOD_WEBHOOK_SECRET over the string t + "." + rawBody.
5. Compare expected with v1 in constant time. Treat malformed hex as a failed check, never as an exception. On failure answer 400 and stop.
6. Only now parse the JSON.
7. GET https://api.paymenthood.com/api/apps/{APP_ID}/payments/referenceId:{referenceId} with the same bearer header, and set the order from paymentState in that response, never from the webhook body.
8. Answer 200 once the order is updated. Keep all of this to a few seconds, because serverless functions can be stopped the moment the response is sent. If the read-back or the update fails, answer 500 so PaymentHood retries.
The endpoint must not require a login, session or token from the caller. PaymentHood has none; the signature is the authentication.
ORDER STATES
- Mark an order paid only when the read-back paymentState is Captured. Failed means the order failed, and the reason is in lastError. Mark refunded only on Refunded.
- Webhooks are retried and can arrive out of order. The handler must be safe to run twice and must never move an order backwards, for example from paid to pending.
- A declined card is not an HTTP error: the API answers 200 with paymentState Failed. Branch on paymentState, not on the status code.
- The return URL is not proof of payment. The customer can close the tab, refresh it, or replay it.
- Refunds, only if I ask later: POST https://api.paymenthood.com/api/apps/{APP_ID}/payments/{paymentId}/refund with the numeric paymentId, not the referenceId, and wait for Refunded.
SELF-CHECK
With secret whsec_test_secret, t 1720000000 and body {"payment":{"paymentId":42,"paymentState":"Captured"}}, the correct v1 is e49595faf1e42b54d3634f76233d28a893e71dc8f1cfdf8c052a42004f0123fb. Use this as a test.
WHEN YOU ARE DONE
List the secrets I still need to add, and give me the one URL PaymentHood will call, so I can test on the deployed app.
v0
Watch for
- Variables without NEXT_PUBLIC_ stay on the server. Redeploy after you change them.
- Use the production URL for webhooks. A protected preview deployment rejects them.
- Read the webhook body with request.text(), not request.json().
Platform notes follow the documentation of v0 and Vercel. Read more: v0: security (opens in new tab), Vercel: environment variables (opens in new tab).
Add PaymentHood checkout to this app. PaymentHood is a hosted checkout: I create a payment on the server, send the customer to a PaymentHood page where they pay, and PaymentHood reports the result to my server by webhook. Card details never touch my app. Reference: https://docs.paymenthood.com/quickstart/
BUILD
1. create-checkout (server): for the signed-in user's order, read the price from my database (never from the browser), create the payment, and return the redirectUrl. The browser then redirects to it.
2. paymenthood-webhook (server, public): receives PaymentHood's notifications and updates the order.
3. A return page at /checkout/return. It shows "Confirming your payment" and re-reads the order from my own database until it is paid, failed, or about a minute has passed. It never marks anything paid itself.
4. An orders table: id (uuid), user id, amount, currency, status (pending, paid, failed, refunded), paymenthood_payment_id, updated_at.
PLATFORM NOTES (v0)
- Use Next.js App Router Route Handlers: app/api/checkout/route.ts and app/api/paymenthood/webhook/route.ts, with export const runtime = "nodejs".
- In the webhook handler read the body with await request.text(). Never call request.json() first.
- Keep the three secrets in the project's environment variables without a NEXT_PUBLIC_ prefix, set for Production and Preview. Changes only apply to new deployments, so redeploy after adding them. Keep the PaymentHood calls out of client components. If you use a server action, put "use server" at the top of the file.
- PaymentHood must reach the webhook on the production URL. A preview deployment can sit behind deployment protection, which would turn every webhook into a 401.
- Store orders in the database this project already uses.
SECRETS
- Three server-only secrets: PAYMENTHOOD_APP_ID, PAYMENTHOOD_API_KEY, PAYMENTHOOD_WEBHOOK_SECRET. Never put them in frontend code, in a VITE_ or NEXT_PUBLIC_ variable, in the repository, or in a log line. Do not ask me to paste them into this chat: tell me which are missing and let me add them through the platform's secure secrets form.
- Where I get them: the App ID and API key are in the PaymentHood Console (https://console.paymenthood.com) for my app. The webhook secret comes from Settings, Webhook, Enable webhook signing. It starts with whsec_ and is shown only once.
- Sandbox and live are separate PaymentHood apps with separate credentials. Start with sandbox.
CREATE THE PAYMENT
POST https://api.paymenthood.com/api/apps/{APP_ID}/payments/hosted-page
Headers: Authorization: Bearer {API_KEY} and Content-Type: application/json
Send all of these every time, including the ones that look optional:
{
"referenceId": "<my order uuid, saved on the order before this call>",
"amount": 20.00,
"currency": "USD",
"returnUrl": "https://<my deployed domain>/checkout/return?order=<order id>",
"autoCapture": true,
"showPayRecurringInCheckout": false,
"registerAutoPayment": false,
"webhookUrl": "<my deployed paymenthood-webhook URL>",
"customerOrder": { "customer": { "customerId": "<my user id>" } }
}
- amount is a decimal in MAJOR units: 20.00 means twenty dollars. Do not convert to cents or multiply by 100, which is the Stripe habit and would charge 100 times too much.
- Success is 201 Created. Redirect the browser to the redirectUrl in the response.
- referenceId is the idempotency key and is unique per app. If the call answers 409, read the payment back (see the webhook steps) instead of creating a second one. If its paymentState is still Created and it has a redirectUrl, send the customer there.
WEBHOOK (do these steps in this order)
PaymentHood POSTs {"payment":{"paymentId":90210,"paymentState":"Captured","paymentStateId":7,"referenceId":"..."}} and nothing else, so the body is only a hint that something changed.
1. Read the raw body as text before any JSON parsing. A re-serialised body never verifies.
2. The header X-PaymentHood-Signature looks like t=1785000000,v1=<64 lowercase hex characters>. Parse it by key, not by position.
3. Reject if t is more than 300 seconds from the current Unix time.
4. expected = lowercase hex of HMAC-SHA256 with the key PAYMENTHOOD_WEBHOOK_SECRET over the string t + "." + rawBody.
5. Compare expected with v1 in constant time. Treat malformed hex as a failed check, never as an exception. On failure answer 400 and stop.
6. Only now parse the JSON.
7. GET https://api.paymenthood.com/api/apps/{APP_ID}/payments/referenceId:{referenceId} with the same bearer header, and set the order from paymentState in that response, never from the webhook body.
8. Answer 200 once the order is updated. Keep all of this to a few seconds, because serverless functions can be stopped the moment the response is sent. If the read-back or the update fails, answer 500 so PaymentHood retries.
The endpoint must not require a login, session or token from the caller. PaymentHood has none; the signature is the authentication.
ORDER STATES
- Mark an order paid only when the read-back paymentState is Captured. Failed means the order failed, and the reason is in lastError. Mark refunded only on Refunded.
- Webhooks are retried and can arrive out of order. The handler must be safe to run twice and must never move an order backwards, for example from paid to pending.
- A declined card is not an HTTP error: the API answers 200 with paymentState Failed. Branch on paymentState, not on the status code.
- The return URL is not proof of payment. The customer can close the tab, refresh it, or replay it.
- Refunds, only if I ask later: POST https://api.paymenthood.com/api/apps/{APP_ID}/payments/{paymentId}/refund with the numeric paymentId, not the referenceId, and wait for Refunded.
SELF-CHECK
With secret whsec_test_secret, t 1720000000 and body {"payment":{"paymentId":42,"paymentState":"Captured"}}, the correct v1 is e49595faf1e42b54d3634f76233d28a893e71dc8f1cfdf8c052a42004f0123fb. Use this as a test.
WHEN YOU ARE DONE
List the secrets I still need to add, and give me the one URL PaymentHood will call, so I can test on the deployed app.
Replit Agent
Watch for
- Secrets added while developing do not carry over to the published app. Add them again in the publishing settings.
- Use the published URL for webhooks, not the development URL.
Platform notes follow Replit's own documentation. Read more: Replit: Secrets (opens in new tab), Replit: publishing (opens in new tab).
Add PaymentHood checkout to this app. PaymentHood is a hosted checkout: I create a payment on the server, send the customer to a PaymentHood page where they pay, and PaymentHood reports the result to my server by webhook. Card details never touch my app. Reference: https://docs.paymenthood.com/quickstart/
BUILD
1. create-checkout (server): for the signed-in user's order, read the price from my database (never from the browser), create the payment, and return the redirectUrl. The browser then redirects to it.
2. paymenthood-webhook (server, public): receives PaymentHood's notifications and updates the order.
3. A return page at /checkout/return. It shows "Confirming your payment" and re-reads the order from my own database until it is paid, failed, or about a minute has passed. It never marks anything paid itself.
4. An orders table: id (uuid), user id, amount, currency, status (pending, paid, failed, refunded), paymenthood_payment_id, updated_at.
PLATFORM NOTES (Replit)
- This is a real server (Express, or Flask or FastAPI). Put the three secrets in Replit Secrets and read them from environment variables. Secrets added while developing do not carry over to the published app, so add all three again in the publishing settings.
- For the webhook route with Express, use express.raw({ type: "application/json" }) on that route only, so the body stays as bytes. With Flask use request.get_data(). Do not put express.json() in front of it.
- Use the published URL (replit.app or my custom domain) for webhookUrl and returnUrl. The development URL changes and is not stable enough for webhooks.
- Store orders in the project's database.
SECRETS
- Three server-only secrets: PAYMENTHOOD_APP_ID, PAYMENTHOOD_API_KEY, PAYMENTHOOD_WEBHOOK_SECRET. Never put them in frontend code, in a VITE_ or NEXT_PUBLIC_ variable, in the repository, or in a log line. Do not ask me to paste them into this chat: tell me which are missing and let me add them through the platform's secure secrets form.
- Where I get them: the App ID and API key are in the PaymentHood Console (https://console.paymenthood.com) for my app. The webhook secret comes from Settings, Webhook, Enable webhook signing. It starts with whsec_ and is shown only once.
- Sandbox and live are separate PaymentHood apps with separate credentials. Start with sandbox.
CREATE THE PAYMENT
POST https://api.paymenthood.com/api/apps/{APP_ID}/payments/hosted-page
Headers: Authorization: Bearer {API_KEY} and Content-Type: application/json
Send all of these every time, including the ones that look optional:
{
"referenceId": "<my order uuid, saved on the order before this call>",
"amount": 20.00,
"currency": "USD",
"returnUrl": "https://<my deployed domain>/checkout/return?order=<order id>",
"autoCapture": true,
"showPayRecurringInCheckout": false,
"registerAutoPayment": false,
"webhookUrl": "<my deployed paymenthood-webhook URL>",
"customerOrder": { "customer": { "customerId": "<my user id>" } }
}
- amount is a decimal in MAJOR units: 20.00 means twenty dollars. Do not convert to cents or multiply by 100, which is the Stripe habit and would charge 100 times too much.
- Success is 201 Created. Redirect the browser to the redirectUrl in the response.
- referenceId is the idempotency key and is unique per app. If the call answers 409, read the payment back (see the webhook steps) instead of creating a second one. If its paymentState is still Created and it has a redirectUrl, send the customer there.
WEBHOOK (do these steps in this order)
PaymentHood POSTs {"payment":{"paymentId":90210,"paymentState":"Captured","paymentStateId":7,"referenceId":"..."}} and nothing else, so the body is only a hint that something changed.
1. Read the raw body as text before any JSON parsing. A re-serialised body never verifies.
2. The header X-PaymentHood-Signature looks like t=1785000000,v1=<64 lowercase hex characters>. Parse it by key, not by position.
3. Reject if t is more than 300 seconds from the current Unix time.
4. expected = lowercase hex of HMAC-SHA256 with the key PAYMENTHOOD_WEBHOOK_SECRET over the string t + "." + rawBody.
5. Compare expected with v1 in constant time. Treat malformed hex as a failed check, never as an exception. On failure answer 400 and stop.
6. Only now parse the JSON.
7. GET https://api.paymenthood.com/api/apps/{APP_ID}/payments/referenceId:{referenceId} with the same bearer header, and set the order from paymentState in that response, never from the webhook body.
8. Answer 200 once the order is updated. Keep all of this to a few seconds, because serverless functions can be stopped the moment the response is sent. If the read-back or the update fails, answer 500 so PaymentHood retries.
The endpoint must not require a login, session or token from the caller. PaymentHood has none; the signature is the authentication.
ORDER STATES
- Mark an order paid only when the read-back paymentState is Captured. Failed means the order failed, and the reason is in lastError. Mark refunded only on Refunded.
- Webhooks are retried and can arrive out of order. The handler must be safe to run twice and must never move an order backwards, for example from paid to pending.
- A declined card is not an HTTP error: the API answers 200 with paymentState Failed. Branch on paymentState, not on the status code.
- The return URL is not proof of payment. The customer can close the tab, refresh it, or replay it.
- Refunds, only if I ask later: POST https://api.paymenthood.com/api/apps/{APP_ID}/payments/{paymentId}/refund with the numeric paymentId, not the referenceId, and wait for Refunded.
SELF-CHECK
With secret whsec_test_secret, t 1720000000 and body {"payment":{"paymentId":42,"paymentState":"Captured"}}, the correct v1 is e49595faf1e42b54d3634f76233d28a893e71dc8f1cfdf8c052a42004f0123fb. Use this as a test.
WHEN YOU ARE DONE
List the secrets I still need to add, and give me the one URL PaymentHood will call, so I can test on the deployed app.
Base44
Watch for
- The webhook runs without a signed-in user, so the signature check is the only caller check. Do not remove it.
- Read secrets inside the handler, not when the module loads.
Platform notes follow Base44's own documentation. Read more: Base44: backend functions (opens in new tab).
Add PaymentHood checkout to this app. PaymentHood is a hosted checkout: I create a payment on the server, send the customer to a PaymentHood page where they pay, and PaymentHood reports the result to my server by webhook. Card details never touch my app. Reference: https://docs.paymenthood.com/quickstart/
BUILD
1. create-checkout (server): for the signed-in user's order, read the price from my database (never from the browser), create the payment, and return the redirectUrl. The browser then redirects to it.
2. paymenthood-webhook (server, public): receives PaymentHood's notifications and updates the order.
3. A return page at /checkout/return. It shows "Confirming your payment" and re-reads the order from my own database until it is paid, failed, or about a minute has passed. It never marks anything paid itself.
4. An orders table: id (uuid), user id, amount, currency, status (pending, paid, failed, refunded), paymenthood_payment_id, updated_at.
PLATFORM NOTES (Base44)
- Use backend functions (Deno) for both server pieces. Each deployed function has its own HTTP endpoint, and that endpoint is the webhook URL.
- Store the three values as app secrets and read them inside the handler.
- A webhook call has no signed-in Base44 user, so inside paymenthood-webhook use the service role to update the order. The signature check is then the only check on who is calling, so it must not be skipped. create-checkout runs as the signed-in user.
SECRETS
- Three server-only secrets: PAYMENTHOOD_APP_ID, PAYMENTHOOD_API_KEY, PAYMENTHOOD_WEBHOOK_SECRET. Never put them in frontend code, in a VITE_ or NEXT_PUBLIC_ variable, in the repository, or in a log line. Do not ask me to paste them into this chat: tell me which are missing and let me add them through the platform's secure secrets form.
- Where I get them: the App ID and API key are in the PaymentHood Console (https://console.paymenthood.com) for my app. The webhook secret comes from Settings, Webhook, Enable webhook signing. It starts with whsec_ and is shown only once.
- Sandbox and live are separate PaymentHood apps with separate credentials. Start with sandbox.
CREATE THE PAYMENT
POST https://api.paymenthood.com/api/apps/{APP_ID}/payments/hosted-page
Headers: Authorization: Bearer {API_KEY} and Content-Type: application/json
Send all of these every time, including the ones that look optional:
{
"referenceId": "<my order uuid, saved on the order before this call>",
"amount": 20.00,
"currency": "USD",
"returnUrl": "https://<my deployed domain>/checkout/return?order=<order id>",
"autoCapture": true,
"showPayRecurringInCheckout": false,
"registerAutoPayment": false,
"webhookUrl": "<my deployed paymenthood-webhook URL>",
"customerOrder": { "customer": { "customerId": "<my user id>" } }
}
- amount is a decimal in MAJOR units: 20.00 means twenty dollars. Do not convert to cents or multiply by 100, which is the Stripe habit and would charge 100 times too much.
- Success is 201 Created. Redirect the browser to the redirectUrl in the response.
- referenceId is the idempotency key and is unique per app. If the call answers 409, read the payment back (see the webhook steps) instead of creating a second one. If its paymentState is still Created and it has a redirectUrl, send the customer there.
WEBHOOK (do these steps in this order)
PaymentHood POSTs {"payment":{"paymentId":90210,"paymentState":"Captured","paymentStateId":7,"referenceId":"..."}} and nothing else, so the body is only a hint that something changed.
1. Read the raw body as text before any JSON parsing. A re-serialised body never verifies.
2. The header X-PaymentHood-Signature looks like t=1785000000,v1=<64 lowercase hex characters>. Parse it by key, not by position.
3. Reject if t is more than 300 seconds from the current Unix time.
4. expected = lowercase hex of HMAC-SHA256 with the key PAYMENTHOOD_WEBHOOK_SECRET over the string t + "." + rawBody.
5. Compare expected with v1 in constant time. Treat malformed hex as a failed check, never as an exception. On failure answer 400 and stop.
6. Only now parse the JSON.
7. GET https://api.paymenthood.com/api/apps/{APP_ID}/payments/referenceId:{referenceId} with the same bearer header, and set the order from paymentState in that response, never from the webhook body.
8. Answer 200 once the order is updated. Keep all of this to a few seconds, because serverless functions can be stopped the moment the response is sent. If the read-back or the update fails, answer 500 so PaymentHood retries.
The endpoint must not require a login, session or token from the caller. PaymentHood has none; the signature is the authentication.
ORDER STATES
- Mark an order paid only when the read-back paymentState is Captured. Failed means the order failed, and the reason is in lastError. Mark refunded only on Refunded.
- Webhooks are retried and can arrive out of order. The handler must be safe to run twice and must never move an order backwards, for example from paid to pending.
- A declined card is not an HTTP error: the API answers 200 with paymentState Failed. Branch on paymentState, not on the status code.
- The return URL is not proof of payment. The customer can close the tab, refresh it, or replay it.
- Refunds, only if I ask later: POST https://api.paymenthood.com/api/apps/{APP_ID}/payments/{paymentId}/refund with the numeric paymentId, not the referenceId, and wait for Refunded.
SELF-CHECK
With secret whsec_test_secret, t 1720000000 and body {"payment":{"paymentId":42,"paymentState":"Captured"}}, the correct v1 is e49595faf1e42b54d3634f76233d28a893e71dc8f1cfdf8c052a42004f0123fb. Use this as a test.
WHEN YOU ARE DONE
List the secrets I still need to add, and give me the one URL PaymentHood will call, so I can test on the deployed app.
Coding agents
Watch for
- Run the tests the prompt asks for before the first real payment.
- If the docs and the OpenAPI spec disagree, the spec wins.
These agents can read documentation directly, so the prompt sends them to it and asks for tests. Read more: PaymentHood: verification samples (opens in new tab).
Add PaymentHood checkout to this repository, using its existing stack and conventions. PaymentHood is a hosted checkout: the server creates a payment, the customer pays on a PaymentHood page, and PaymentHood reports the result to the server by webhook. Card details never touch this code.
Read these first: https://docs.paymenthood.com/quickstart/, https://docs.paymenthood.com/payments/ and https://docs.paymenthood.com/webhooks/signing/. The OpenAPI spec at https://api.paymenthood.com/swagger/v1/swagger.json is the contract if anything disagrees.
BUILD
1. create-checkout (server): for the signed-in user's order, read the price from my database (never from the browser), create the payment, and return the redirectUrl. The browser then redirects to it.
2. paymenthood-webhook (server, public): receives PaymentHood's notifications and updates the order.
3. A return page at /checkout/return. It shows "Confirming your payment" and re-reads the order from my own database until it is paid, failed, or about a minute has passed. It never marks anything paid itself.
4. An orders table: id (uuid), user id, amount, currency, status (pending, paid, failed, refunded), paymenthood_payment_id, updated_at.
PLATFORM NOTES
- Follow the stack, folder layout and error-handling style already in this repository.
- Write tests for: (1) the known-answer signature below, (2) a stale timestamp is rejected, (3) a tampered body is rejected, (4) a Failed event arriving after Captured does not move the order back, (5) an amount of 20.00 reaches the API as 20.00 and not 2000.
- Check your verification code against PaymentHood's own receivers (Node, PHP, Python and C#) at https://docs.paymenthood.com/webhooks/verification-samples/ rather than inventing a scheme.
SECRETS
- Three server-only secrets: PAYMENTHOOD_APP_ID, PAYMENTHOOD_API_KEY, PAYMENTHOOD_WEBHOOK_SECRET. Read them from environment variables. Add them to .env.example with empty values, make sure .env is git-ignored, and never print them or put them in client code.
- Where I get them: the App ID and API key are in the PaymentHood Console (https://console.paymenthood.com) for my app. The webhook secret comes from Settings, Webhook, Enable webhook signing. It starts with whsec_ and is shown only once.
- Sandbox and live are separate PaymentHood apps with separate credentials. Start with sandbox.
CREATE THE PAYMENT
POST https://api.paymenthood.com/api/apps/{APP_ID}/payments/hosted-page
Headers: Authorization: Bearer {API_KEY} and Content-Type: application/json
Send all of these every time, including the ones that look optional:
{
"referenceId": "<my order uuid, saved on the order before this call>",
"amount": 20.00,
"currency": "USD",
"returnUrl": "https://<my deployed domain>/checkout/return?order=<order id>",
"autoCapture": true,
"showPayRecurringInCheckout": false,
"registerAutoPayment": false,
"webhookUrl": "<my deployed paymenthood-webhook URL>",
"customerOrder": { "customer": { "customerId": "<my user id>" } }
}
- amount is a decimal in MAJOR units: 20.00 means twenty dollars. Do not convert to cents or multiply by 100, which is the Stripe habit and would charge 100 times too much.
- Success is 201 Created. Redirect the browser to the redirectUrl in the response.
- referenceId is the idempotency key and is unique per app. If the call answers 409, read the payment back (see the webhook steps) instead of creating a second one. If its paymentState is still Created and it has a redirectUrl, send the customer there.
WEBHOOK (do these steps in this order)
PaymentHood POSTs {"payment":{"paymentId":90210,"paymentState":"Captured","paymentStateId":7,"referenceId":"..."}} and nothing else, so the body is only a hint that something changed.
1. Read the raw body as text before any JSON parsing. A re-serialised body never verifies.
2. The header X-PaymentHood-Signature looks like t=1785000000,v1=<64 lowercase hex characters>. Parse it by key, not by position.
3. Reject if t is more than 300 seconds from the current Unix time.
4. expected = lowercase hex of HMAC-SHA256 with the key PAYMENTHOOD_WEBHOOK_SECRET over the string t + "." + rawBody.
5. Compare expected with v1 in constant time. Treat malformed hex as a failed check, never as an exception. On failure answer 400 and stop.
6. Only now parse the JSON.
7. GET https://api.paymenthood.com/api/apps/{APP_ID}/payments/referenceId:{referenceId} with the same bearer header, and set the order from paymentState in that response, never from the webhook body.
8. Answer 200 once the order is updated. Keep all of this to a few seconds, because serverless functions can be stopped the moment the response is sent. If the read-back or the update fails, answer 500 so PaymentHood retries.
The endpoint must not require a login, session or token from the caller. PaymentHood has none; the signature is the authentication.
ORDER STATES
- Mark an order paid only when the read-back paymentState is Captured. Failed means the order failed, and the reason is in lastError. Mark refunded only on Refunded.
- Webhooks are retried and can arrive out of order. The handler must be safe to run twice and must never move an order backwards, for example from paid to pending.
- A declined card is not an HTTP error: the API answers 200 with paymentState Failed. Branch on paymentState, not on the status code.
- The return URL is not proof of payment. The customer can close the tab, refresh it, or replay it.
- Refunds, only if I ask later: POST https://api.paymenthood.com/api/apps/{APP_ID}/payments/{paymentId}/refund with the numeric paymentId, not the referenceId, and wait for Refunded.
SELF-CHECK
With secret whsec_test_secret, t 1720000000 and body {"payment":{"paymentId":42,"paymentState":"Captured"}}, the correct v1 is e49595faf1e42b54d3634f76233d28a893e71dc8f1cfdf8c052a42004f0123fb. Use this as a test.
WHEN YOU ARE DONE
Summarise what you changed, which secrets I still need to set, and the one URL PaymentHood will call.
PaymentHood is not affiliated with, endorsed by or sponsored by any of these products. Their names and logos belong to their owners and appear here only to show which tool a prompt is written for.
Six rules generated payment code usually breaks
A tool that is only asked to add payments will tend to guess. Each prompt states these outright instead.
Amounts in major units
20.00 means twenty dollars. Tools used to Stripe send cents, and a checkout that multiplies by 100 charges a hundred times too much.
Keys stay on the server
The App ID, API key and webhook secret live in the platform's secret store, never in frontend code, a public variable, or the chat.
The signature is checked on the raw body
A webhook is trusted only after its HMAC verifies against the exact bytes received, inside a five minute window. A re-encoded body never matches.
The API has the last word
The webhook only says which payment changed. The order is updated from reading the payment back, never from the webhook body.
Safe to run twice
Retries and out-of-order delivery are normal. An order never moves backwards, and a repeated create cannot produce a second payment.
Paid means Captured
The return URL proves nothing. Fulfilment waits for the Captured state, and a declined card is read from the state, not from the HTTP status.
Everything the prompts rely on is in the docs
Each prompt is built from these pages. If a tool asks you a question the prompt does not answer, the answer is here, and the OpenAPI reference wins if anything ever disagrees.
docs.paymenthood.com (opens in new tab)