# Removing Gateman

## Our No lock-in guarantee

Gateman is designed to make working with external services easier, not to make
your application dependent on us.

Wherever possible, Gateman uses the provider's native request formats rather
than proprietary abstractions. Your provider accounts, credentials, and
resources remain yours.

If you decide to stop using Gateman, you can connect directly to your
providers through their APIs or SDKs. You will need to replace Gateman's
authentication and any services it was performing for you, but you will not be
locked into a Gateman-specific provider integration.

The examples below show direct provider calls in PHP, Node.js, Python, and Go.
They use placeholders and are real provider requests when you run them. This
verification checks syntax, request shape, and error handling without sending
any live provider requests. Some integrations reuse native parameters directly;
others require provider-specific mapping.

## Replacing Gateman with direct provider access

The common change is to replace the signed Gateman request with the provider's
own authentication and API or SDK call. Supply credentials through the secure
configuration mechanism already chosen for your application, handle the
provider's native response and errors, and replace any Gateman-specific
capture, accounting, idempotency, tracking, or secret-retrieval behavior that
your application still needs.

Gateman application environments are not the same thing as provider
production, test, or sandbox modes. Choose the provider mode explicitly when
you move the call into your application.

## Payments

### Stripe — `stripe:use` and `POST /stripe/request`

Gateman sends the nested native Stripe request after adding authentication and
selecting the configured environment. Direct access keeps the `/v1/` path,
URL-encoded parameters, and Stripe `Idempotency-Key`; only the transport and
credential handling change.

The examples create a PaymentIntent when run. Configure `STRIPE_SECRET_KEY`
only when adapting an example for a Stripe test account.

<!-- gateman:code-tabs -->
### PHP
```php
$ch = curl_init('https://api.stripe.com/v1/payment_intents');
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
    CURLOPT_USERPWD => getenv('STRIPE_SECRET_KEY') . ':',
    CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded', 'Idempotency-Key: order-123'],
    CURLOPT_POSTFIELDS => http_build_query(['amount' => 1000, 'currency' => 'usd'], '', '&', PHP_QUERY_RFC3986)]);
$response = curl_exec($ch);
if ($response === false || curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) throw new RuntimeException('Stripe request failed');
echo $response;
```
### Node.js
```javascript
const response = await fetch('https://api.stripe.com/v1/payment_intents', {
  method: 'POST', headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`, 'Content-Type': 'application/x-www-form-urlencoded', 'Idempotency-Key': 'order-123' },
  body: new URLSearchParams({ amount: '1000', currency: 'usd' })
});
if (!response.ok) throw new Error(`Stripe HTTP ${response.status}`);
console.log(await response.json());
```
### Python
```python
import os, urllib.request, urllib.parse, base64, json
body = urllib.parse.urlencode({'amount': '1000', 'currency': 'usd'}).encode()
request = urllib.request.Request('https://api.stripe.com/v1/payment_intents', data=body, method='POST', headers={
    'Authorization': 'Basic ' + base64.b64encode((os.environ['STRIPE_SECRET_KEY'] + ':').encode()).decode(),
    'Content-Type': 'application/x-www-form-urlencoded', 'Idempotency-Key': 'order-123'})
with urllib.request.urlopen(request) as response:
    print(json.load(response))
```
### Go
```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
)

func main() {
	form := url.Values{"amount": {"1000"}, "currency": {"usd"}}
	req, err := http.NewRequest("POST", "https://api.stripe.com/v1/payment_intents", strings.NewReader(form.Encode()))
	if err != nil {
		panic(err)
	}
	req.SetBasicAuth(os.Getenv("STRIPE_SECRET_KEY"), "")
	req.Header.Set("Idempotency-Key", "order-123")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		panic(fmt.Sprintf("Stripe HTTP %s", res.Status))
	}
	b, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}
```
<!-- gateman:end-code-tabs -->

Stripe's API version is selected by the account/request headers and SDK
configuration, not by Gateman's `environment` label. Stripe documents
[PaymentIntents](https://docs.stripe.com/api/payment_intents/create) and
[idempotent requests](https://docs.stripe.com/api/idempotent_requests).

### Square — `square:use` and `POST /square/request`

This read-only example lists locations. Direct access uses the Square access
token and the production or sandbox origin selected by the application.

<!-- gateman:code-tabs -->
### PHP
```php
$ch = curl_init('https://connect.squareupsandbox.com/v2/locations');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('SQUARE_ACCESS_TOKEN_TEST'), 'Square-Version: 2026-09-16']]);
$response = curl_exec($ch); if ($response === false || curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) throw new RuntimeException('Square request failed'); echo $response;
```
### Node.js
```javascript
const response = await fetch('https://connect.squareupsandbox.com/v2/locations', { headers: { Authorization: `Bearer ${process.env.SQUARE_ACCESS_TOKEN_TEST}`, 'Square-Version': '2026-09-16' } });
if (!response.ok) throw new Error(`Square HTTP ${response.status}`); console.log(await response.json());
```
### Python
```python
import os, urllib.request, json
request = urllib.request.Request('https://connect.squareupsandbox.com/v2/locations', headers={'Authorization': 'Bearer ' + os.environ['SQUARE_ACCESS_TOKEN_TEST'], 'Square-Version': '2026-09-16'})
with urllib.request.urlopen(request) as response:
    if response.status >= 400: raise RuntimeError(f'Square HTTP {response.status}')
    print(json.load(response))
```
### Go
```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("GET", "https://connect.squareupsandbox.com/v2/locations", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("SQUARE_ACCESS_TOKEN_TEST"))
	req.Header.Set("Square-Version", "2026-09-16")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		panic(fmt.Sprintf("Square HTTP %s", res.Status))
	}
	b, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}
```
<!-- gateman:end-code-tabs -->

See Square's [API reference](https://developer.squareup.com/reference/square)
and [SDK documentation](https://developer.squareup.com/docs/sdks).

### PayPal — `paypal:use` and `POST /paypal/request`

PayPal is not authenticated like Stripe or Square. Direct access first obtains
an OAuth 2.0 access token, then calls a native PayPal REST endpoint. The
`PayPal-Request-Id` header is the provider's idempotency mechanism for
supported mutations.

The following examples obtain a token and create a sandbox Orders v2 request
shape. They are examples only and must not be run against a live account.

<!-- gateman:code-tabs -->
### PHP
```php
$id = getenv('PAYPAL_CLIENT_ID'); $secret = getenv('PAYPAL_CLIENT_SECRET');
$token = curl_init('https://api-m.sandbox.paypal.com/v1/oauth2/token'); curl_setopt_array($token, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_USERPWD => "$id:$secret", CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/x-www-form-urlencoded'], CURLOPT_POSTFIELDS => 'grant_type=client_credentials']); $tokenResponse=curl_exec($token); if ($tokenResponse===false || curl_getinfo($token,CURLINFO_RESPONSE_CODE)>=400) throw new RuntimeException('PayPal OAuth request failed'); $tokenBody = json_decode($tokenResponse, true, 512, JSON_THROW_ON_ERROR);
$body = json_encode(['intent'=>'CAPTURE','purchase_units'=>[['amount'=>['currency_code'=>'USD','value'=>'10.00']]]], JSON_THROW_ON_ERROR); $ch = curl_init('https://api-m.sandbox.paypal.com/v2/checkout/orders'); curl_setopt_array($ch, [CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_HTTPHEADER=>['Authorization: Bearer '.$tokenBody['access_token'],'Content-Type: application/json','PayPal-Request-Id: example-order-123'],CURLOPT_POSTFIELDS=>$body]); $response=curl_exec($ch); if ($response===false || curl_getinfo($ch,CURLINFO_RESPONSE_CODE)>=400) throw new RuntimeException('PayPal Orders request failed'); echo $response;
```
### Node.js
```javascript
const basic = Buffer.from(`${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`).toString('base64');
const tokenResponse = await fetch('https://api-m.sandbox.paypal.com/v1/oauth2/token', { method: 'POST', headers: { Authorization: `Basic ${basic}`, 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'grant_type=client_credentials' }); if (!tokenResponse.ok) throw new Error(`PayPal OAuth HTTP ${tokenResponse.status}`); const token = await tokenResponse.json();
const response = await fetch('https://api-m.sandbox.paypal.com/v2/checkout/orders', { method: 'POST', headers: { Authorization: `Bearer ${token.access_token}`, 'Content-Type': 'application/json', 'PayPal-Request-Id': 'example-order-123' }, body: JSON.stringify({ intent: 'CAPTURE', purchase_units: [{ amount: { currency_code: 'USD', value: '10.00' } }] }) }); if (!response.ok) throw new Error(`PayPal Orders HTTP ${response.status}`); console.log(await response.json());
```
### Python
```python
import os, urllib.request, urllib.parse, urllib.error, base64, json
auth = base64.b64encode(f"{os.environ['PAYPAL_CLIENT_ID']}:{os.environ['PAYPAL_CLIENT_SECRET']}".encode()).decode()
token_req = urllib.request.Request('https://api-m.sandbox.paypal.com/v1/oauth2/token', data=b'grant_type=client_credentials', method='POST', headers={'Authorization':'Basic '+auth,'Content-Type':'application/x-www-form-urlencoded'})
with urllib.request.urlopen(token_req) as response: token = json.load(response)['access_token']
body = json.dumps({'intent':'CAPTURE','purchase_units':[{'amount':{'currency_code':'USD','value':'10.00'}}]}).encode(); req = urllib.request.Request('https://api-m.sandbox.paypal.com/v2/checkout/orders', data=body, method='POST', headers={'Authorization':'Bearer '+token,'Content-Type':'application/json','PayPal-Request-Id':'example-order-123'})
with urllib.request.urlopen(req) as response:
    if response.status >= 400: raise RuntimeError(f'PayPal Orders HTTP {response.status}')
    print(json.load(response))
```
### Go
```go
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
)

func main() {
	basic := base64.StdEncoding.EncodeToString([]byte(os.Getenv("PAYPAL_CLIENT_ID") + ":" + os.Getenv("PAYPAL_CLIENT_SECRET")))
	tokenReq, err := http.NewRequest("POST", "https://api-m.sandbox.paypal.com/v1/oauth2/token", strings.NewReader(url.Values{"grant_type": {"client_credentials"}}.Encode()))
	if err != nil {
		panic(err)
	}
	tokenReq.Header.Set("Authorization", "Basic "+basic)
	tokenReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	tokenRes, err := http.DefaultClient.Do(tokenReq)
	if err != nil {
		panic(err)
	}
	defer tokenRes.Body.Close()
	if tokenRes.StatusCode >= 400 {
		panic(fmt.Sprintf("PayPal OAuth HTTP %s", tokenRes.Status))
	}
	var token struct {
		AccessToken string `json:"access_token"`
	}
	if err := json.NewDecoder(tokenRes.Body).Decode(&token); err != nil {
		panic(err)
	}
	body, err := json.Marshal(map[string]any{"intent": "CAPTURE", "purchase_units": []any{map[string]any{"amount": map[string]string{"currency_code": "USD", "value": "10.00"}}}})
	if err != nil {
		panic(err)
	}
	orderReq, err := http.NewRequest("POST", "https://api-m.sandbox.paypal.com/v2/checkout/orders", bytes.NewReader(body))
	if err != nil {
		panic(err)
	}
	orderReq.Header.Set("Authorization", "Bearer "+token.AccessToken)
	orderReq.Header.Set("Content-Type", "application/json")
	orderReq.Header.Set("PayPal-Request-Id", "example-order-123")
	res, err := http.DefaultClient.Do(orderReq)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		panic(fmt.Sprintf("PayPal Orders HTTP %s", res.Status))
	}
	out, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(out))
}
```
<!-- gateman:end-code-tabs -->

PayPal's
[OAuth documentation](https://developer.paypal.com/api/rest/authentication),
[Orders API](https://developer.paypal.com/api/rest/integration/orders-api),
and [request-id guidance](https://developer.paypal.com/api/rest/requests/)
are authoritative for the complete flow.

## Email

Gateman's `email.send` routes accept a common mail intent but map it to each
provider. Production sends contact the provider; staging and development use
Gateman capture. Direct integrations must choose their own test mode and
handle any Gateman-specific ledger, suppression, tracking, or idempotency
behavior they still require.

### Amazon SES — `POST /send/ses`

This uses the SES API v2 `SendEmail` operation through each provider's native
HTTP interface. The sender identity must be verified in SES. Attachments and
raw MIME messages require additional SES structure and are intentionally not
included in this minimal example.

<!-- gateman:code-tabs -->
### PHP
```php
$body = json_encode(['FromEmailAddress'=>getenv('SES_FROM_EMAIL'),'Destination'=>['ToAddresses'=>['recipient@example.test']],'Content'=>['Simple'=>['Subject'=>['Data'=>'Test subject'],'Body'=>['Text'=>['Data'=>'Test body']]]]], JSON_THROW_ON_ERROR);
require 'vendor/autoload.php'; $client=new Aws\SesV2\SesV2Client(['version'=>'latest','region'=>getenv('AWS_REGION')]); $result=$client->sendEmail(json_decode($body,true,512,JSON_THROW_ON_ERROR)); echo $result->get('MessageId');
```
### Node.js
```javascript
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2'; const result=await new SESv2Client({region:process.env.AWS_REGION}).send(new SendEmailCommand({FromEmailAddress:process.env.SES_FROM_EMAIL,Destination:{ToAddresses:['recipient@example.test']},Content:{Simple:{Subject:{Data:'Test subject'},Body:{Text:{Data:'Test body'}}}}})); console.log(result.MessageId);
```
### Python
```python
import boto3, os
client = boto3.client('sesv2', region_name=os.environ['AWS_REGION'])
result = client.send_email(FromEmailAddress=os.environ['SES_FROM_EMAIL'], Destination={'ToAddresses':['recipient@example.test']}, Content={'Simple': {'Subject': {'Data':'Test subject'}, 'Body': {'Text': {'Data':'Test body'}}}})
print(result['MessageId'])
```
### Go
```go
package main

import (
	"context"
	"fmt"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/sesv2"
	"github.com/aws/aws-sdk-go-v2/service/sesv2/types"
	"os"
)

func main() {
	cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(os.Getenv("AWS_REGION")))
	if err != nil {
		panic(err)
	}
	out, err := sesv2.NewFromConfig(cfg).SendEmail(context.Background(), &sesv2.SendEmailInput{FromEmailAddress: &[]string{os.Getenv("SES_FROM_EMAIL")}[0], Destination: &types.Destination{ToAddresses: []string{"recipient@example.test"}}, Content: &types.EmailContent{Simple: &types.Message{Subject: &types.Content{Data: &[]string{"Test subject"}[0]}, Body: &types.Body{Text: &types.Content{Data: &[]string{"Test body"}[0]}}}}})
	if err != nil {
		panic(err)
	}
	fmt.Println(*out.MessageId)
}
```
<!-- gateman:end-code-tabs -->

All four blocks use the official SES v2 SDK; the SDK performs the provider's
authenticated request. See [SES SendEmail](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html)
and [SES attachments](https://docs.aws.amazon.com/ses/latest/dg/attachments.html).

### Postmark — `POST /send/postmark`

Postmark uses `X-Postmark-Server-Token` and its native field names. The API
returns `MessageID`.

<!-- gateman:code-tabs -->
### PHP
```php
$body=json_encode(['From'=>getenv('POSTMARK_FROM'),'To'=>'recipient@example.test','Subject'=>'Test subject','TextBody'=>'Test body'],JSON_THROW_ON_ERROR); $ch=curl_init('https://api.postmarkapp.com/email'); curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_HTTPHEADER=>['Accept: application/json','Content-Type: application/json','X-Postmark-Server-Token: '.getenv('POSTMARK_SERVER_TOKEN')],CURLOPT_POSTFIELDS=>$body]); $response=curl_exec($ch); if ($response===false || curl_getinfo($ch,CURLINFO_RESPONSE_CODE)>=400) throw new RuntimeException('Postmark request failed'); echo $response;
```
### Node.js
```javascript
const response = await fetch('https://api.postmarkapp.com/email', { method:'POST', headers:{Accept:'application/json','Content-Type':'application/json','X-Postmark-Server-Token':process.env.POSTMARK_SERVER_TOKEN}, body:JSON.stringify({From:process.env.POSTMARK_FROM,To:'recipient@example.test',Subject:'Test subject',TextBody:'Test body'}) }); if (!response.ok) throw new Error(`Postmark HTTP ${response.status}`); console.log(await response.json());
```
### Python
```python
import os, urllib.request, json
body=json.dumps({'From':os.environ['POSTMARK_FROM'],'To':'recipient@example.test','Subject':'Test subject','TextBody':'Test body'}).encode(); req=urllib.request.Request('https://api.postmarkapp.com/email',data=body,method='POST',headers={'Accept':'application/json','Content-Type':'application/json','X-Postmark-Server-Token':os.environ['POSTMARK_SERVER_TOKEN']});
with urllib.request.urlopen(req) as response:
    if response.status >= 400: raise RuntimeError(f'Postmark HTTP {response.status}')
    print(json.load(response)['MessageID'])
```
### Go
```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	body := []byte(`{"From":"` + os.Getenv("POSTMARK_FROM") + `","To":"recipient@example.test","Subject":"Test subject","TextBody":"Test body"}`)
	req, err := http.NewRequest("POST", "https://api.postmarkapp.com/email", bytes.NewReader(body))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Postmark-Server-Token", os.Getenv("POSTMARK_SERVER_TOKEN"))
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		panic(fmt.Sprintf("Postmark HTTP %s", res.Status))
	}
	b, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}
```
<!-- gateman:end-code-tabs -->

Official reference: [Postmark Email API](https://postmarkapp.com/developer/api/email-api).

### Resend — `POST /send/resend`

Resend uses bearer authentication and a native JSON body. The response contains
the provider email identifier.

<!-- gateman:code-tabs -->
### PHP
```php
$body=json_encode(['from'=>getenv('RESEND_FROM'),'to'=>['recipient@example.test'],'subject'=>'Test subject','text'=>'Test body'],JSON_THROW_ON_ERROR); $ch=curl_init('https://api.resend.com/emails'); curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_HTTPHEADER=>['Authorization: Bearer '.getenv('RESEND_API_KEY'),'Content-Type: application/json'],CURLOPT_POSTFIELDS=>$body]); $response=curl_exec($ch); if ($response===false || curl_getinfo($ch,CURLINFO_RESPONSE_CODE)>=400) throw new RuntimeException('Resend request failed'); echo $response;
```
### Node.js
```javascript
const response=await fetch('https://api.resend.com/emails',{method:'POST',headers:{Authorization:`Bearer ${process.env.RESEND_API_KEY}`,'Content-Type':'application/json'},body:JSON.stringify({from:process.env.RESEND_FROM,to:['recipient@example.test'],subject:'Test subject',text:'Test body'})}); if (!response.ok) throw new Error(`Resend HTTP ${response.status}`); console.log(await response.json());
```
### Python
```python
import os, urllib.request, json
body=json.dumps({'from':os.environ['RESEND_FROM'],'to':['recipient@example.test'],'subject':'Test subject','text':'Test body'}).encode(); req=urllib.request.Request('https://api.resend.com/emails',data=body,method='POST',headers={'Authorization':'Bearer '+os.environ['RESEND_API_KEY'],'Content-Type':'application/json'});
with urllib.request.urlopen(req) as response:
    if response.status >= 400: raise RuntimeError(f'Resend HTTP {response.status}')
    print(json.load(response))
```
### Go
```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	body := []byte(`{"from":"` + os.Getenv("RESEND_FROM") + `","to":["recipient@example.test"],"subject":"Test subject","text":"Test body"}`)
	req, err := http.NewRequest("POST", "https://api.resend.com/emails", bytes.NewReader(body))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("RESEND_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		panic(fmt.Sprintf("Resend HTTP %s", res.Status))
	}
	b, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}
```
<!-- gateman:end-code-tabs -->

Official reference: [Resend Email API](https://resend.com/docs/api-reference/emails/send-email).

## SMS

Direct SMS integrations use each provider's own sender/origination rules,
responses, retry behavior, and test environment. Gateman's staging/development
capture and capability-specific idempotency do not automatically follow the
application.

### Twilio — `POST /twilio/sms/send`

Twilio's native Messages fields remain recognizable. The direct request uses
HTTP Basic authentication with the account/API-key credentials.

<!-- gateman:code-tabs -->
### PHP
```php
$ch=curl_init('https://api.twilio.com/2010-04-01/Accounts/'.rawurlencode(getenv('TWILIO_ACCOUNT_SID')).'/Messages.json'); curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_USERPWD=>getenv('TWILIO_API_KEY_SID').':'.getenv('TWILIO_API_KEY_SECRET'),CURLOPT_POSTFIELDS=>http_build_query(['To'=>'+15558675310','From'=>'+15557122661','Body'=>'Example message'],'','&',PHP_QUERY_RFC3986)]); $response=curl_exec($ch); if ($response===false || curl_getinfo($ch,CURLINFO_RESPONSE_CODE)>=400) throw new RuntimeException('Twilio request failed'); echo $response;
```
### Node.js
```javascript
const auth=Buffer.from(`${process.env.TWILIO_API_KEY_SID}:${process.env.TWILIO_API_KEY_SECRET}`).toString('base64'); const response=await fetch(`https://api.twilio.com/2010-04-01/Accounts/${process.env.TWILIO_ACCOUNT_SID}/Messages.json`,{method:'POST',headers:{Authorization:`Basic ${auth}`,'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({To:'+15558675310',From:'+15557122661',Body:'Example message'})}); if (!response.ok) throw new Error(`Twilio HTTP ${response.status}`); console.log(await response.json());
```
### Python
```python
import os, urllib.request, urllib.parse, base64, json
body=urllib.parse.urlencode({'To':'+15558675310','From':'+15557122661','Body':'Example message'}).encode(); auth=base64.b64encode((os.environ['TWILIO_API_KEY_SID']+':'+os.environ['TWILIO_API_KEY_SECRET']).encode()).decode(); req=urllib.request.Request('https://api.twilio.com/2010-04-01/Accounts/'+os.environ['TWILIO_ACCOUNT_SID']+'/Messages.json',data=body,method='POST',headers={'Authorization':'Basic '+auth,'Content-Type':'application/x-www-form-urlencoded'});
with urllib.request.urlopen(req) as response:
    if response.status >= 400: raise RuntimeError(f'Twilio HTTP {response.status}')
    print(json.load(response))
```
### Go
```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
)

func main() {
	form := url.Values{"To": {"+15558675310"}, "From": {"+15557122661"}, "Body": {"Example message"}}
	req, err := http.NewRequest("POST", "https://api.twilio.com/2010-04-01/Accounts/"+os.Getenv("TWILIO_ACCOUNT_SID")+"/Messages.json", strings.NewReader(form.Encode()))
	if err != nil {
		panic(err)
	}
	req.SetBasicAuth(os.Getenv("TWILIO_API_KEY_SID"), os.Getenv("TWILIO_API_KEY_SECRET"))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		panic(fmt.Sprintf("Twilio HTTP %s", res.Status))
	}
	b, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}
```
<!-- gateman:end-code-tabs -->

See Twilio's [Messages API](https://www.twilio.com/docs/messaging/api/message-resource).

### Telnyx — `POST /telnyx/sms/send`

Telnyx uses a native JSON message at `https://api.telnyx.com/v2/messages`.

<!-- gateman:code-tabs -->
### PHP
```php
$body=json_encode(['to'=>'+15558675310','messaging_profile_id'=>getenv('TELNYX_MESSAGING_PROFILE_ID'),'text'=>'Example message'],JSON_THROW_ON_ERROR); $ch=curl_init('https://api.telnyx.com/v2/messages'); curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_HTTPHEADER=>['Authorization: Bearer '.getenv('TELNYX_API_KEY'),'Content-Type: application/json'],CURLOPT_POSTFIELDS=>$body]); $response=curl_exec($ch); if ($response===false || curl_getinfo($ch,CURLINFO_RESPONSE_CODE)>=400) throw new RuntimeException('Telnyx request failed'); echo $response;
```
### Node.js
```javascript
const response=await fetch('https://api.telnyx.com/v2/messages',{method:'POST',headers:{Authorization:`Bearer ${process.env.TELNYX_API_KEY}`,'Content-Type':'application/json'},body:JSON.stringify({to:'+15558675310',messaging_profile_id:process.env.TELNYX_MESSAGING_PROFILE_ID,text:'Example message'})}); if (!response.ok) throw new Error(`Telnyx HTTP ${response.status}`); console.log(await response.json());
```
### Python
```python
import os, urllib.request, json
body=json.dumps({'to':'+15558675310','messaging_profile_id':os.environ['TELNYX_MESSAGING_PROFILE_ID'],'text':'Example message'}).encode(); req=urllib.request.Request('https://api.telnyx.com/v2/messages',data=body,method='POST',headers={'Authorization':'Bearer '+os.environ['TELNYX_API_KEY'],'Content-Type':'application/json'});
with urllib.request.urlopen(req) as response:
    if response.status >= 400: raise RuntimeError(f'Telnyx HTTP {response.status}')
    print(json.load(response))
```
### Go
```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	body := []byte(`{"to":"+15558675310","messaging_profile_id":"` + os.Getenv("TELNYX_MESSAGING_PROFILE_ID") + `","text":"Example message"}`)
	req, err := http.NewRequest("POST", "https://api.telnyx.com/v2/messages", bytes.NewReader(body))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("TELNYX_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		panic(fmt.Sprintf("Telnyx HTTP %s", res.Status))
	}
	b, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}
```
<!-- gateman:end-code-tabs -->

See Telnyx's [messaging documentation](https://developers.telnyx.com/docs/messaging/messages).

### AWS End User Messaging SMS — `POST /aws/sms/send`

Gateman calls the AWS `SendTextMessage` operation. Direct use requires AWS
credentials, a region, an origination identity or pool, and the AWS SDK's
SigV4 handling.

<!-- gateman:code-tabs -->
### PHP
```php
require 'vendor/autoload.php'; use Aws\PinpointSMSVoiceV2\PinpointSMSVoiceV2Client;
$client=new PinpointSMSVoiceV2Client(['version'=>'2022-03-31','region'=>getenv('AWS_REGION')]); $result=$client->sendTextMessage(['DestinationPhoneNumber'=>'+15558675310','OriginationIdentity'=>getenv('AWS_ORIGINATION_IDENTITY'),'MessageBody'=>'Example message','MessageType'=>'TRANSACTIONAL']); echo $result->get('MessageId');
```
### Node.js
```javascript
import { PinpointSMSVoiceV2Client, SendTextMessageCommand } from '@aws-sdk/client-pinpoint-sms-voice-v2';
const client=new PinpointSMSVoiceV2Client({region:process.env.AWS_REGION}); const result=await client.send(new SendTextMessageCommand({DestinationPhoneNumber:'+15558675310',OriginationIdentity:process.env.AWS_ORIGINATION_IDENTITY,MessageBody:'Example message',MessageType:'TRANSACTIONAL'})); console.log(result.MessageId);
```
### Python
```python
import boto3, os
result=boto3.client('pinpoint-sms-voice-v2',region_name=os.environ['AWS_REGION']).send_text_message(DestinationPhoneNumber='+15558675310',OriginationIdentity=os.environ['AWS_ORIGINATION_IDENTITY'],MessageBody='Example message',MessageType='TRANSACTIONAL')
print(result['MessageId'])
```
### Go
```go
package main

import (
	"context"
	"fmt"
	"github.com/aws/aws-sdk-go-v2/config"
	sms "github.com/aws/aws-sdk-go-v2/service/pinpointsmsvoicev2"
	"os"
)

func main() {
	cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(os.Getenv("AWS_REGION")))
	if err != nil {
		panic(err)
	}
	out, err := sms.NewFromConfig(cfg).SendTextMessage(context.Background(), &sms.SendTextMessageInput{DestinationPhoneNumber: &[]string{"+15558675310"}[0], OriginationIdentity: &[]string{os.Getenv("AWS_ORIGINATION_IDENTITY")}[0], MessageBody: &[]string{"Example message"}[0], MessageType: "TRANSACTIONAL"})
	if err != nil {
		panic(err)
	}
	fmt.Println(*out.MessageId)
}
```
<!-- gateman:end-code-tabs -->

See AWS's [SendTextMessage API](https://docs.aws.amazon.com/pinpoint/latest/apireference_smsvoicev2/API_SendTextMessage.html).

## Storage

### Amazon S3 — `s3:use` and `POST /s3/presign`

Gateman signs a native `GetObject` or `PutObject` request and returns a URL.
Direct access moves that signing operation into the application. Object bytes
still transfer directly between the application and S3.

<!-- gateman:code-tabs -->
### PHP
```php
require 'vendor/autoload.php'; use Aws\S3\S3Client;
$s3=new S3Client(['version'=>'latest','region'=>getenv('AWS_REGION')]); $command=$s3->getCommand('GetObject',['Bucket'=>getenv('S3_BUCKET'),'Key'=>'reports/example.pdf']); echo (string)$s3->createPresignedRequest($command,'+15 minutes')->getUri();
```
### Node.js
```javascript
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const url=await getSignedUrl(new S3Client({region:process.env.AWS_REGION}),new GetObjectCommand({Bucket:process.env.S3_BUCKET,Key:'reports/example.pdf'}),{expiresIn:900}); console.log(url);
```
### Python
```python
import boto3, os
print(boto3.client('s3',region_name=os.environ['AWS_REGION']).generate_presigned_url('get_object',Params={'Bucket':os.environ['S3_BUCKET'],'Key':'reports/example.pdf'},ExpiresIn=900))
```
### Go
```go
package main

import (
	"context"
	"fmt"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"os"
)

func main() {
	cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(os.Getenv("AWS_REGION")))
	if err != nil {
		panic(err)
	}
	p := s3.NewPresignClient(s3.NewFromConfig(cfg))
	out, err := p.PresignGetObject(context.Background(), &s3.GetObjectInput{Bucket: aws.String(os.Getenv("S3_BUCKET")), Key: aws.String("reports/example.pdf")})
	if err != nil {
		panic(err)
	}
	fmt.Println(out.URL)
}
```
<!-- gateman:end-code-tabs -->

See [AWS S3 presigned URLs](https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/s3-presigned-url.html).

### Cloudflare R2 — `r2:use` and `POST /r2/presign`

R2 uses an S3-compatible presigner. Use the account-specific S3 endpoint,
R2 S3 credentials, and `region: auto`. A jurisdiction-specific endpoint is
required when the bucket belongs to a defined jurisdiction.

<!-- gateman:code-tabs -->
### PHP
```php
require 'vendor/autoload.php'; use Aws\S3\S3Client;
$s3=new S3Client(['version'=>'latest','region'=>'auto','endpoint'=>'https://'.getenv('R2_ACCOUNT_ID').'.r2.cloudflarestorage.com','credentials'=>['key'=>getenv('R2_ACCESS_KEY_ID'),'secret'=>getenv('R2_SECRET_ACCESS_KEY')]]); $command=$s3->getCommand('GetObject',['Bucket'=>getenv('R2_BUCKET'),'Key'=>'reports/example.pdf']); echo (string)$s3->createPresignedRequest($command,'+15 minutes')->getUri();
```
### Node.js
```javascript
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const client=new S3Client({region:'auto',endpoint:`https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,credentials:{accessKeyId:process.env.R2_ACCESS_KEY_ID,secretAccessKey:process.env.R2_SECRET_ACCESS_KEY}}); console.log(await getSignedUrl(client,new GetObjectCommand({Bucket:process.env.R2_BUCKET,Key:'reports/example.pdf'}),{expiresIn:900}));
```
### Python
```python
import boto3, os
client=boto3.client('s3',region_name='auto',endpoint_url=f"https://{os.environ['R2_ACCOUNT_ID']}.r2.cloudflarestorage.com",aws_access_key_id=os.environ['R2_ACCESS_KEY_ID'],aws_secret_access_key=os.environ['R2_SECRET_ACCESS_KEY']); print(client.generate_presigned_url('get_object',Params={'Bucket':os.environ['R2_BUCKET'],'Key':'reports/example.pdf'},ExpiresIn=900))
```
### Go
```go
package main

import (
	"context"
	"fmt"
	aws "github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"os"
)

func main() {
	cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion("auto"), config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(os.Getenv("R2_ACCESS_KEY_ID"), os.Getenv("R2_SECRET_ACCESS_KEY"), "")))
	if err != nil {
		panic(err)
	}
	client := s3.NewFromConfig(cfg, func(o *s3.Options) {
		o.BaseEndpoint = aws.String("https://" + os.Getenv("R2_ACCOUNT_ID") + ".r2.cloudflarestorage.com")
		o.UsePathStyle = true
	})
	p := s3.NewPresignClient(client)
	out, err := p.PresignGetObject(context.Background(), &s3.GetObjectInput{Bucket: aws.String(os.Getenv("R2_BUCKET")), Key: aws.String("reports/example.pdf")})
	if err != nil {
		panic(err)
	}
	fmt.Println(out.URL)
}
```
<!-- gateman:end-code-tabs -->

See Cloudflare's [R2 S3 API](https://developers.cloudflare.com/r2/api/s3/),
[presigned URLs](https://developers.cloudflare.com/r2/api/s3/presigned-urls/),
and [R2 credentials](https://developers.cloudflare.com/r2/api/tokens/).

## AWS and Cloudflare administration

### AWS — `aws:use` and `POST /aws/request`

Gateman accepts a native AWS HTTP request and adds SigV4 signing. Direct code
can use an AWS SDK, which is generally safer than reproducing SigV4 manually.
This read-only example lists IAM roles.

<!-- gateman:code-tabs -->
### PHP
```php
require 'vendor/autoload.php'; $iam=new Aws\Iam\IamClient(['version'=>'latest','region'=>getenv('AWS_REGION')]); foreach($iam->getPaginator('ListRoles') as $page) foreach($page['Roles'] as $role) echo $role['RoleName']."\n";
```
### Node.js
```javascript
import { IAMClient, ListRolesCommand } from '@aws-sdk/client-iam'; const result=await new IAMClient({region:process.env.AWS_REGION}).send(new ListRolesCommand({})); console.log(result.Roles?.map(role=>role.RoleName));
```
### Python
```python
import boto3, os
for page in boto3.client('iam',region_name=os.environ['AWS_REGION']).get_paginator('list_roles').paginate(): print([role['RoleName'] for role in page['Roles']])
```
### Go
```go
package main

import (
	"context"
	"fmt"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/iam"
	"os"
)

func main() {
	cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(os.Getenv("AWS_REGION")))
	if err != nil {
		panic(err)
	}
	out, err := iam.NewFromConfig(cfg).ListRoles(context.Background(), &iam.ListRolesInput{})
	if err != nil {
		panic(err)
	}
	for _, role := range out.Roles {
		fmt.Println(*role.RoleName)
	}
}
```
<!-- gateman:end-code-tabs -->

The SDK call is the direct equivalent of constructing the AWS native request;
the Gateman outer JSON envelope is not an AWS SDK argument.

### Cloudflare — `cloudflare:use` and `POST /cloudflare/request`

Direct access uses a Cloudflare API token and the API v4 origin. The provider
path, query, JSON body, and response remain recognizable. This read-only
example lists accounts.

<!-- gateman:code-tabs -->
### PHP
```php
$ch=curl_init('https://api.cloudflare.com/client/v4/accounts'); curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_HTTPHEADER=>['Authorization: Bearer '.getenv('CLOUDFLARE_API_TOKEN'),'Accept: application/json']]); $response=curl_exec($ch); if ($response===false || curl_getinfo($ch,CURLINFO_RESPONSE_CODE)>=400) throw new RuntimeException('Cloudflare request failed'); echo $response;
```
### Node.js
```javascript
const response=await fetch('https://api.cloudflare.com/client/v4/accounts',{headers:{Authorization:`Bearer ${process.env.CLOUDFLARE_API_TOKEN}`,Accept:'application/json'}}); if (!response.ok) throw new Error(`Cloudflare HTTP ${response.status}`); console.log(await response.json());
```
### Python
```python
import os, urllib.request, json
req=urllib.request.Request('https://api.cloudflare.com/client/v4/accounts',headers={'Authorization':'Bearer '+os.environ['CLOUDFLARE_API_TOKEN'],'Accept':'application/json'});
with urllib.request.urlopen(req) as response:
    if response.status >= 400: raise RuntimeError(f'Cloudflare HTTP {response.status}')
    print(json.load(response))
```
### Go
```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("GET", "https://api.cloudflare.com/client/v4/accounts", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CLOUDFLARE_API_TOKEN"))
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		panic(fmt.Sprintf("Cloudflare HTTP %s", res.Status))
	}
	b, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}
```
<!-- gateman:end-code-tabs -->

See Cloudflare's [API documentation](https://developers.cloudflare.com/api/).

## Anti-bot and credential retrieval

### Turnstile and reCAPTCHA

`anti_bot.verify` returns configured site and secret keys. It does not receive
visitor tokens or perform verification. Removing Gateman means configuring the
keys directly through the application's chosen secure mechanism and calling
the provider's verification endpoint from the trusted backend.

The examples below verify tokens directly. `TURNSTILE_TOKEN` and
`RECAPTCHA_TOKEN` are application-provided values obtained from the submitted
form; they are not Gateman values.

<!-- gateman:code-tabs -->
### PHP
```php
$tokenFromYourForm=getenv('TURNSTILE_TOKEN'); if (!$tokenFromYourForm) throw new RuntimeException('TURNSTILE_TOKEN is required'); $ch=curl_init('https://challenges.cloudflare.com/turnstile/v0/siteverify'); curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_POSTFIELDS=>http_build_query(['secret'=>getenv('TURNSTILE_SECRET_KEY'),'response'=>$tokenFromYourForm])]); $resultBody=curl_exec($ch); $status=curl_getinfo($ch,CURLINFO_RESPONSE_CODE); if ($resultBody===false || $status>=400) throw new RuntimeException('Turnstile HTTP request failed'); $result=json_decode($resultBody,true,512,JSON_THROW_ON_ERROR); var_dump($result['success'] ?? false);
```
### Node.js
```javascript
const tokenFromYourForm=process.env.TURNSTILE_TOKEN; if (!tokenFromYourForm) throw new Error('TURNSTILE_TOKEN is required'); const response=await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({secret:process.env.TURNSTILE_SECRET_KEY,response:tokenFromYourForm})}); if (!response.ok) throw new Error(`Turnstile HTTP ${response.status}`); console.log(await response.json());
```
### Python
```python
import os, urllib.error, urllib.parse, urllib.request, json
token_from_your_form=os.environ['TURNSTILE_TOKEN']; req=urllib.request.Request('https://challenges.cloudflare.com/turnstile/v0/siteverify',data=urllib.parse.urlencode({'secret':os.environ['TURNSTILE_SECRET_KEY'],'response':token_from_your_form}).encode(),method='POST');
try:
    with urllib.request.urlopen(req) as response:
        if response.status >= 400: raise RuntimeError(f'Turnstile HTTP {response.status}')
        print(json.load(response))
except urllib.error.HTTPError as error:
    raise RuntimeError(f'Turnstile HTTP {error.code}') from error
```
### Go
```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
)

func main() {
	token := os.Getenv("TURNSTILE_TOKEN")
	if token == "" {
		panic("TURNSTILE_TOKEN is required")
	}
	form := url.Values{"secret": {os.Getenv("TURNSTILE_SECRET_KEY")}, "response": {token}}
	req, err := http.NewRequest("POST", "https://challenges.cloudflare.com/turnstile/v0/siteverify", strings.NewReader(form.Encode()))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		panic(fmt.Sprintf("Turnstile HTTP %s", res.Status))
	}
	b, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}
```
<!-- gateman:end-code-tabs -->

### Google reCAPTCHA — `siteverify`

Google reCAPTCHA verification uses the application-provided token and the
secret key at Google's `siteverify` endpoint. Check `success` and, for score-
based keys, the expected `action` and `hostname` in application code.

<!-- gateman:code-tabs -->
### PHP
```php
$token=getenv('RECAPTCHA_TOKEN'); if (!$token) throw new RuntimeException('RECAPTCHA_TOKEN is required'); $ch=curl_init('https://www.google.com/recaptcha/api/siteverify'); curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_POSTFIELDS=>http_build_query(['secret'=>getenv('RECAPTCHA_SECRET_KEY'),'response'=>$token])]); $body=curl_exec($ch); $status=curl_getinfo($ch,CURLINFO_RESPONSE_CODE); if ($body===false || $status>=400) throw new RuntimeException('reCAPTCHA HTTP request failed'); $result=json_decode($body,true,512,JSON_THROW_ON_ERROR); var_dump($result['success'] ?? false);
```
### Node.js
```javascript
const token=process.env.RECAPTCHA_TOKEN; if (!token) throw new Error('RECAPTCHA_TOKEN is required'); const response=await fetch('https://www.google.com/recaptcha/api/siteverify',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({secret:process.env.RECAPTCHA_SECRET_KEY,response:token})}); if (!response.ok) throw new Error(`reCAPTCHA HTTP ${response.status}`); console.log(await response.json());
```
### Python
```python
import json, os, urllib.error, urllib.parse, urllib.request
token=os.environ['RECAPTCHA_TOKEN']; req=urllib.request.Request('https://www.google.com/recaptcha/api/siteverify',data=urllib.parse.urlencode({'secret':os.environ['RECAPTCHA_SECRET_KEY'],'response':token}).encode(),method='POST')
try:
    with urllib.request.urlopen(req) as response:
        if response.status >= 400: raise RuntimeError(f'reCAPTCHA HTTP {response.status}')
        print(json.load(response))
except urllib.error.HTTPError as error:
    raise RuntimeError(f'reCAPTCHA HTTP {error.code}') from error
```
### Go
```go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
)

func main() {
	token := os.Getenv("RECAPTCHA_TOKEN")
	if token == "" {
		panic("RECAPTCHA_TOKEN is required")
	}
	form := url.Values{"secret": {os.Getenv("RECAPTCHA_SECRET_KEY")}, "response": {token}}
	req, err := http.NewRequest("POST", "https://www.google.com/recaptcha/api/siteverify", strings.NewReader(form.Encode()))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		panic(fmt.Sprintf("reCAPTCHA HTTP %s", res.Status))
	}
	body, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	var result map[string]any
	if err := json.Unmarshal(body, &result); err != nil {
		panic(err)
	}
	fmt.Println(result["success"])
}
```
<!-- gateman:end-code-tabs -->

The browser widget, token lifecycle, hostname/action/score policy, and final
allow/deny decision remain application responsibilities. See [Turnstile
validation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/)
and [reCAPTCHA verification](https://developers.google.com/recaptcha/docs/verify).

### Stored credentials and database passwords

`secrets:read` and `db:read` are Gateman storage capabilities, not provider
APIs. If they are removed, the application must obtain the required values
from a customer-selected source. No particular secrets manager, environment
layout, database architecture, or deployment model is required by this guide.

### Stripe webhook secrets and setup

`POST /stripe/webhook-secret` retrieves a Gateman-stored signing secret; it is
not a Stripe API request. `POST /stripe/webhook/setup` configures a supported
Stripe endpoint using the application's credentials. Direct integrations can
use Stripe's webhook endpoint API or dashboard, then store the returned secret
and verify events in the application's own receiver. Do not change or delete
an existing endpoint merely because Gateman is no longer used.

See Stripe's [webhook endpoint API](https://docs.stripe.com/api/webhook_endpoints)
and [signature verification](https://docs.stripe.com/webhooks/signature).

The read-only example below lists existing webhook endpoints. It demonstrates
the direct Stripe authentication and response handling without creating,
updating, or deleting a webhook endpoint.

<!-- gateman:code-tabs -->
### PHP
```php
$ch=curl_init('https://api.stripe.com/v1/webhook_endpoints?limit=10'); curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_USERPWD=>getenv('STRIPE_SECRET_KEY').':',CURLOPT_HTTPHEADER=>['Accept: application/json']]); $body=curl_exec($ch); $status=curl_getinfo($ch,CURLINFO_RESPONSE_CODE); if ($body===false || $status>=400) throw new RuntimeException('Stripe webhook HTTP request failed'); echo $body;
```
### Node.js
```javascript
const response=await fetch('https://api.stripe.com/v1/webhook_endpoints?limit=10',{headers:{Authorization:`Bearer ${process.env.STRIPE_SECRET_KEY}`,Accept:'application/json'}}); if (!response.ok) throw new Error(`Stripe webhook HTTP ${response.status}`); console.log(await response.json());
```
### Python
```python
import base64, json, os, urllib.error, urllib.request
request=urllib.request.Request('https://api.stripe.com/v1/webhook_endpoints?limit=10',headers={'Authorization':'Basic '+base64.b64encode((os.environ['STRIPE_SECRET_KEY']+':').encode()).decode(),'Accept':'application/json'})
try:
    with urllib.request.urlopen(request) as response:
        if response.status >= 400: raise RuntimeError(f'Stripe webhook HTTP {response.status}')
        print(json.load(response))
except urllib.error.HTTPError as error:
    raise RuntimeError(f'Stripe webhook HTTP {error.code}') from error
```
### Go
```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("GET", "https://api.stripe.com/v1/webhook_endpoints?limit=10", nil)
	if err != nil {
		panic(err)
	}
	req.SetBasicAuth(os.Getenv("STRIPE_SECRET_KEY"), "")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		panic(fmt.Sprintf("Stripe webhook HTTP %s", res.Status))
	}
	body, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(body))
}
```
<!-- gateman:end-code-tabs -->

## WordPress

A WordPress customer can use another mail transport or payment integration
instead of the GetGateman integration. The appropriate change depends on the
site's existing configuration; removing the plugin, store data, orders, or
provider resources is not a universal requirement. See [GetGateman for
WordPress](https://docs.getgateman.com/wordpress/).

## Related references

- [Send email](https://docs.getgateman.com/capabilities/email.send/)
- [Send SMS](https://docs.getgateman.com/capabilities/sms.send/)
- [Use Stripe](https://docs.getgateman.com/capabilities/stripe.use/)
- [Use Square](https://docs.getgateman.com/capabilities/square.use/)
- [Use PayPal](https://docs.getgateman.com/capabilities/paypal.use/)
- [Use S3](https://docs.getgateman.com/capabilities/s3.use/)
- [Use Cloudflare R2](https://docs.getgateman.com/capabilities/r2.use/)
- [Use AWS](https://docs.getgateman.com/capabilities/aws.use/)
- [Use Cloudflare](https://docs.getgateman.com/capabilities/cloudflare.use/)
- [Webhooks and event handling](https://docs.getgateman.com/webhooks-and-event-handling/)
