A minimal Gr4vy Build-Your-Own-Connector adapter built with FastAPI. It implements every BYOC endpoint and uses a penny simulator so you can exercise every response path — success, decline, service error, pending / redirect — without connecting to a real payment provider.
Replace the simulator functions in app/simulator.py with calls to your actual provider API to turn this into a production connector.
app/
├── config.py # Environment variables (SECRET_KEY, HOST, PORT)
├── auth.py # HMAC-SHA256 signature verification (Gr4vy signs every request)
├── schemas.py # Pydantic request models — one per BYOC endpoint
├── responses.py # Pydantic response models — mirrors gr4vy/connector/custom/schemas.py
├── simulator.py # Penny simulator — replace these functions with real provider logic
└── main.py # FastAPI routes — wire requests to simulator / provider
# 1. Install
pip install -e .
# 2. Set the shared secret (must match what you enter in the Gr4vy dashboard)
export SECRET_KEY=my-very-secret-key
# 3. Run
uvicorn app.main:app --reload --port 8000
# or:
serveThe adapter listens on http://0.0.0.0:8000 by default. You can override HOST and PORT with environment variables.
To expose a local server to Gr4vy's sandbox, use a tunnelling tool such as ngrok:
ngrok http 8000
# Copy the HTTPS URL, e.g. https://abc123.ngrok.ioThen configure the connection in the Gr4vy dashboard with:
- URL:
https://abc123.ngrok.io - Secret Key:
my-very-secret-key
Gr4vy calls GET /verify-credentials. If it returns 200, the connector is active.
The last two digits of amount control the response for all amount-based endpoints:
| Penny | Endpoint | Outcome |
|---|---|---|
01 |
all | Decline: INSUFFICIENT_FUNDS / REFUND_DECLINED |
02 |
all | Decline: DO_NOT_HONOR |
03 |
all | Service error: SERVICE_ERROR (HTTP 502) |
00 |
create-transaction | Pending — buyer redirected to /simulator/approve |
52 |
create-refund | Pending — async settlement (refund delayed) |
09 |
create-transaction | Immediate capture regardless of intent |
| other | all | Succeed per intent (authorize or capture) |
Pass "store": true in the POST /create-transaction request. When the transaction succeeds the response includes an embedded payment_method with a token, causing Gr4vy to emit both a TransactionAuthorizationSucceeded event and a TokenSucceeded event.
- Create a transaction with
amountending in51(e.g.1051). - The response contains
status: "pending"andpayment_method.approval_urlpointing toGET /simulator/approve?redirect_url=...&xid=.... /simulator/approveimmediately redirects the buyer back toredirect_url.- Gr4vy receives the callback and calls
POST /approve-transaction. - The transaction resolves to authorized or captured.
Token endpoints (approve-token, delete-token) have no amount field. The simulator reads the test penny from the payment_service_transaction_id (xid), which was generated by POST /create-token.
To trigger a specific outcome, pass "external_identifier" in the POST /create-token request body:
{ "external_identifier": "01" }The penny is encoded in the xid (format sim_{penny:02d}_{hex16}). When Gr4vy later calls approve-token or delete-token the adapter reads it back.
| Penny | approve-token |
delete-token |
|---|---|---|
01 |
CANCELLED_BUYER_APPROVAL |
SERVICE_ERROR |
02 |
DO_NOT_HONOR |
DO_NOT_HONOR |
03 |
SERVICE_ERROR |
SERVICE_ERROR |
| other | Succeed | Succeed |
The adapter is deliberately structured so that the only file you need to touch is app/simulator.py. Each function maps one-to-one to a BYOC endpoint and returns a typed Pydantic model from app/responses.py.
1. Set your provider credentials
Add them to app/config.py (or as environment variables):
# app/config.py
import os
SECRET_KEY: str = os.environ["SECRET_KEY"]
PROVIDER_API_KEY: str = os.environ["PROVIDER_API_KEY"]
PROVIDER_BASE_URL: str = os.environ.get("PROVIDER_BASE_URL", "https://api.myprovider.com")2. Replace each simulator function
Each function in simulator.py has the same signature and return type. Swap out the body:
# Before (simulator):
def simulate_create_transaction(amount, intent, approval_url, *, store=False):
_check_penny(amount)
...
return TransactionResponse(status="succeeded", ...)
# After (real provider):
async def simulate_create_transaction(amount, intent, approval_url, *, store=False):
response = await provider_client.create_charge(amount=amount, intent=intent)
if response.status == "declined":
raise DeclineError("INSUFFICIENT_FUNDS", response.decline_reason)
return TransactionResponse(
status="succeeded",
payment_service_transaction_id=response.charge_id,
authorized_amount=amount,
)Note: Change the function signatures from
deftoasync defif your provider client is async. FastAPI route handlers alreadyawaitthe simulator functions once they are coroutines.
3. Update main.py to await the calls
If you make the simulator functions async, update each route handler:
# main.py — before
result = simulator.simulate_create_transaction(...)
# main.py — after
result = await simulator.simulate_create_transaction(...)4. Handle webhooks
In main.py, the /webhook handler passes the raw provider body to your code:
@app.post("/webhook", dependencies=[_sig])
async def webhook(request: Request) -> WebhookResponse:
raw_body = await request.body()
events = parse_provider_webhook(raw_body, request.headers)
return WebhookResponse(events=events)Build each event using the typed classes in app/responses.py (e.g. TransactionCaptureSucceededEvent, RefundSucceededEvent). Gr4vy doesn't verify that the webhook came from the payment provider, use the payment provider recommended way to verify
app/auth.py verifies the X-BYOC-Timestamp / X-BYOC-Signature headers on every request. This should remain unchanged — it protects your adapter from spoofed requests.
app/schemas.py defines what Gr4vy sends to each endpoint. All models use extra="allow" so new fields added by Gr4vy in the future are accepted without breaking your adapter. You do not need to modify this file unless you want to add stricter validation.
app/responses.py defines every field Gr4vy reads from your responses — including all webhook event payload types. Fields you don't populate are simply None and are excluded from the JSON output. You only need to set the fields relevant to your provider.
| Variable | Default | Description |
|---|---|---|
SECRET_KEY |
development |
Shared secret set in the Gr4vy dashboard |
HOST |
0.0.0.0 |
Bind address for the dev server |
PORT |
8000 |
Port for the dev server |
In production, always set
SECRET_KEYto a long random string and serve over HTTPS.
| Method | Path | Description |
|---|---|---|
GET |
/verify-credentials |
Connectivity check — called on connection save |
POST |
/create-transaction |
New transaction |
POST |
/approve-transaction |
Complete transaction after buyer redirect |
POST |
/capture-transaction |
Delayed or partial capture |
POST |
/create-refund |
Refund a captured transaction |
POST |
/create-token |
Store a payment method |
POST |
/approve-token |
Complete tokenization after buyer redirect |
POST |
/delete-token |
Delete a stored payment method |
POST |
/sync |
Fetch current transaction status |
POST |
/webhook |
Receive raw provider webhook forwarded by Gr4vy |
GET |
/simulator/approve |
Test helper — simulates provider redirect page |
Interactive API docs are available at http://localhost:8000/docs when the server is running.