Repository files navigation

getpayin

CIPyPIPythonLicense

Official server-side Python SDK for the GetPayIn payment integration API. It wraps every integration endpoint with an idiomatic, typed API and computes the order-sensitive HMAC-SHA256 signatures for you, so you never have to build them by hand.

  • Checkouts (invoices.create)
  • Payment operations (payments.void / refund / settle / reverse_authorization / check_status)
  • Server-to-server card charges (vcc.charge)
  • Card tokenization (cards.tokenize / charge / revoke)
  • Recurring mandates (recurring.create / status / cancel / pause / resume)
  • Webhook signature verification (webhooks.verify)

Server-side only. Signing uses your secret hash_token. Never ship it to a browser or mobile client.

Requirements

  • Python 3.8+
  • Zero runtime dependencies — the default transport is built on the standard library

Install

pip install getpayin

Quick start

importosfromgetpayinimportGetpayinClientgetpayin=GetpayinClient(
public_token=os.environ["GETPAYIN_PUBLIC_TOKEN"],
hash_token=os.environ["GETPAYIN_HASH_TOKEN"], # secret — server-side only# base_url defaults to https://pay.getpayin.com# timeout defaults to 30.0 seconds (per attempt)# max_retries defaults to 2 (set 0 to disable retries)
)
checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00", # pass amounts as strings to control the exact wire formcurrency="USD",
redirection_url="https://shop.example.com/return",
webhook_url="https://shop.example.com/webhooks/getpayin",
)
# Redirect the payer to the hosted checkout:print(checkout.checkout_url, checkout.invoice_id, checkout.expires_at)

For an embedded checkout you can render inside your own page, pass iframe=True (sent in the request body but excluded from the signature, like payment_mode):

checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00",
currency="USD",
iframe=True, # enable embedded/iframe checkout
)

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='getpayin_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

Both credentials are issued in the GetPayIn dashboard under Settings → Payment Integrations. public_token is sent on every request; hash_token is the secret used only to sign — it never leaves your server.

Payment operations

getpayin.payments.void(invoice_id=123)
getpayin.payments.settle(invoice_id=123, amount="50.00")
getpayin.payments.reverse_authorization(invoice_id=123)
status=getpayin.payments.check_status(invoice_id=123)
# PaymentResult(invoice_id=123, paid_status='PAID', auth_code='...')# Refunds are idempotent when you pass an idempotency key — safe to retry:refund=getpayin.payments.refund(
invoice_id=123, amount="10.50", idempotency_key="refund-order-1234"
)
# RefundResult(..., refund_amount=10.5)

Card tokenization

result=getpayin.cards.tokenize(
first_name="Jane", last_name="Doe",
card_number="4111111111111111",
card_expiry_month="12", card_expiry_year="2030", card_cvv="123",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.charge(
card_token=result.token,
initiator="merchant",
first_name="Jane", last_name="Doe",
currency="USD", price="100.00", product="Monthly rebill",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.revoke(card_token=result.token)

For US and CA billing addresses, also pass the state fields the API requires: us_state + postal_code (US) or canada_state + postal_code (CA).

Recurring mandates

mandate=getpayin.recurring.create(
first_name="Sam", last_name="Doe", email="sam@example.com",
order_title="Gold subscription",
order_amount="250.00", currency="USD",
cadence_interval="month", cadence_count=1, total_cycles=12,
consent_text="I authorise recurring monthly charges.",
idempotency_key="sub-signup-42",
)
getpayin.recurring.status(mandate.mandate_id)
getpayin.recurring.pause(mandate.mandate_id)
getpayin.recurring.resume(mandate.mandate_id)
getpayin.recurring.cancel(mandate.mandate_id)

Idempotency

Retrying a write after a network error or timeout risks performing it twice. To make that safe, pass an idempotency_key — the SDK sends it as the Idempotency-Key header and the server returns the original result instead of charging, refunding, or creating a second time. Keys are scoped per integration and capped at 64 characters.

MethodA replay with the same key returns
invoices.createthe original invoice and checkout_url
vcc.chargethe original charge
cards.chargethe original charge
payments.refundthe original refund
recurring.createthe original mandate

Reusing a key with a different request — for example recurring.create with changed terms, or payments.refund for a different amount — is rejected as a conflict: a GetpayinApiError with is_idempotency_conflict set (HTTP 409). Only the methods above honor the header.

Verifying webhooks

Pass the parsed body (a dict) or the raw JSON string/bytes to verify. It recomputes the signature with your hash_token and compares in constant time.

fromflaskimportFlask, request, abortfromgetpayinimportGetpayinClient, GetpayinSignatureErrorgetpayin=GetpayinClient(public_token=..., hash_token=...)
app=Flask(__name__)
@app.post("/webhooks/getpayin")defwebhook():
try:
event=getpayin.webhooks.verify(request.get_data(as_text=True))
exceptGetpayinSignatureError:
abort(400)
# event.event, event.invoice_id, event.success, event.raw, ...return"", 200

GetPayIn webhook signatures carry no timestamp, so verification does not protect against replay. Pair it with your own idempotency keyed on invoice_id.

Error handling

Every failure is a subclass of GetpayinError:

ErrorWhen
GetpayinConfigErrorInvalid client configuration (missing tokens, non-positive timeout).
GetpayinApiErrorThe API returned an error. Carries status, errors, raw, retry_after_seconds, and the is_idempotency_conflict / is_rate_limited / is_forbidden flags.
GetpayinSignatureErrorA webhook signature did not verify.
GetpayinConnectionErrorNetwork failure or timeout (no HTTP response).
fromgetpayinimportGetpayinApiErrortry:
getpayin.payments.refund(invoice_id=123, amount="10.00")
exceptGetpayinApiErroraserror:
iferror.is_idempotency_conflict:
... # a refund with this idempotency key already exists

Retries and rate limiting

Every integration endpoint is rate limited server-side, so 429s are an expected condition under burst traffic. The SDK retries transient failures — 429, 5xx, connection errors, and timeouts — with exponential backoff and full jitter, honoring the server's Retry-After header when present.

A request is only ever replayed when replaying it cannot double-charge:

ReplayedNot replayed
All GETs (recurring.status)vcc.charge, cards.charge, cards.tokenize
Any call you pass an idempotency_key toinvoices.create, recurring.create without a key
payments.check_status (a pure read)recurring.cancel / pause / resume

So to make a refund safely retryable, pass an idempotency key — otherwise a failed refund surfaces immediately and is yours to handle. Tune or disable retries per client with max_retries (set 0 to turn them off). timeout applies to each attempt, so worst-case wall time is roughly (max_retries + 1) × timeout plus backoff. For requests the SDK will not replay, GetpayinApiError.retry_after_seconds exposes the server's backoff hint.

Amounts and precision

Signatures are computed over the exact bytes sent on the wire. To avoid any floating-point ambiguity, pass monetary amounts as strings (e.g. "10.50"). Numbers are accepted and stringified, but strings give you full control.

Custom transport

The default transport uses the standard library. Inject any callable matching getpayin.Transport — for a proxy-aware or connection-pooled HTTP client, or a mock in tests:

deftransport(method, url, headers, body, timeout):
resp=requests.request(method, url, headers=headers, data=body, timeout=timeout)
returngetpayin.HttpResponse(status=resp.status_code, text=resp.text, headers=dict(resp.headers))
getpayin=GetpayinClient(public_token=..., hash_token=..., transport=transport)

API reference

The full HTTP API — endpoints, fields, error codes, and test cards — is documented in the GetPayIn API reference: https://pay.getpayin.com/docs/payment_integration/index.html

Contributing

See CONTRIBUTING.md — in particular the note on signed-field ordering, which is the one thing that must stay in lockstep with the server.

Security issues: see SECURITY.md. Please do not open a public issue for a vulnerability.

License

MIT

About

Official server-side Python SDK for the PayLink / GetPayIn payment integration API.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

getpayin

CIPyPIPythonLicense

Official server-side Python SDK for the GetPayIn payment integration API. It wraps every integration endpoint with an idiomatic, typed API and computes the order-sensitive HMAC-SHA256 signatures for you, so you never have to build them by hand.

  • Checkouts (invoices.create)
  • Payment operations (payments.void / refund / settle / reverse_authorization / check_status)
  • Server-to-server card charges (vcc.charge)
  • Card tokenization (cards.tokenize / charge / revoke)
  • Recurring mandates (recurring.create / status / cancel / pause / resume)
  • Webhook signature verification (webhooks.verify)

Server-side only. Signing uses your secret hash_token. Never ship it to a browser or mobile client.

Requirements

  • Python 3.8+
  • Zero runtime dependencies — the default transport is built on the standard library

Install

pip install getpayin

Quick start

importosfromgetpayinimportGetpayinClientgetpayin=GetpayinClient(
public_token=os.environ["GETPAYIN_PUBLIC_TOKEN"],
hash_token=os.environ["GETPAYIN_HASH_TOKEN"], # secret — server-side only# base_url defaults to https://pay.getpayin.com# timeout defaults to 30.0 seconds (per attempt)# max_retries defaults to 2 (set 0 to disable retries)
)
checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00", # pass amounts as strings to control the exact wire formcurrency="USD",
redirection_url="https://shop.example.com/return",
webhook_url="https://shop.example.com/webhooks/getpayin",
)
# Redirect the payer to the hosted checkout:print(checkout.checkout_url, checkout.invoice_id, checkout.expires_at)

For an embedded checkout you can render inside your own page, pass iframe=True (sent in the request body but excluded from the signature, like payment_mode):

checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00",
currency="USD",
iframe=True, # enable embedded/iframe checkout
)

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='getpayin_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

Both credentials are issued in the GetPayIn dashboard under Settings → Payment Integrations. public_token is sent on every request; hash_token is the secret used only to sign — it never leaves your server.

Payment operations

getpayin.payments.void(invoice_id=123)
getpayin.payments.settle(invoice_id=123, amount="50.00")
getpayin.payments.reverse_authorization(invoice_id=123)
status=getpayin.payments.check_status(invoice_id=123)
# PaymentResult(invoice_id=123, paid_status='PAID', auth_code='...')# Refunds are idempotent when you pass an idempotency key — safe to retry:refund=getpayin.payments.refund(
invoice_id=123, amount="10.50", idempotency_key="refund-order-1234"
)
# RefundResult(..., refund_amount=10.5)

Card tokenization

result=getpayin.cards.tokenize(
first_name="Jane", last_name="Doe",
card_number="4111111111111111",
card_expiry_month="12", card_expiry_year="2030", card_cvv="123",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.charge(
card_token=result.token,
initiator="merchant",
first_name="Jane", last_name="Doe",
currency="USD", price="100.00", product="Monthly rebill",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.revoke(card_token=result.token)

For US and CA billing addresses, also pass the state fields the API requires: us_state + postal_code (US) or canada_state + postal_code (CA).

Recurring mandates

mandate=getpayin.recurring.create(
first_name="Sam", last_name="Doe", email="sam@example.com",
order_title="Gold subscription",
order_amount="250.00", currency="USD",
cadence_interval="month", cadence_count=1, total_cycles=12,
consent_text="I authorise recurring monthly charges.",
idempotency_key="sub-signup-42",
)
getpayin.recurring.status(mandate.mandate_id)
getpayin.recurring.pause(mandate.mandate_id)
getpayin.recurring.resume(mandate.mandate_id)
getpayin.recurring.cancel(mandate.mandate_id)

Idempotency

Retrying a write after a network error or timeout risks performing it twice. To make that safe, pass an idempotency_key — the SDK sends it as the Idempotency-Key header and the server returns the original result instead of charging, refunding, or creating a second time. Keys are scoped per integration and capped at 64 characters.

MethodA replay with the same key returns
invoices.createthe original invoice and checkout_url
vcc.chargethe original charge
cards.chargethe original charge
payments.refundthe original refund
recurring.createthe original mandate

Reusing a key with a different request — for example recurring.create with changed terms, or payments.refund for a different amount — is rejected as a conflict: a GetpayinApiError with is_idempotency_conflict set (HTTP 409). Only the methods above honor the header.

Verifying webhooks

Pass the parsed body (a dict) or the raw JSON string/bytes to verify. It recomputes the signature with your hash_token and compares in constant time.

fromflaskimportFlask, request, abortfromgetpayinimportGetpayinClient, GetpayinSignatureErrorgetpayin=GetpayinClient(public_token=..., hash_token=...)
app=Flask(__name__)
@app.post("/webhooks/getpayin")defwebhook():
try:
event=getpayin.webhooks.verify(request.get_data(as_text=True))
exceptGetpayinSignatureError:
abort(400)
# event.event, event.invoice_id, event.success, event.raw, ...return"", 200

GetPayIn webhook signatures carry no timestamp, so verification does not protect against replay. Pair it with your own idempotency keyed on invoice_id.

Error handling

Every failure is a subclass of GetpayinError:

ErrorWhen
GetpayinConfigErrorInvalid client configuration (missing tokens, non-positive timeout).
GetpayinApiErrorThe API returned an error. Carries status, errors, raw, retry_after_seconds, and the is_idempotency_conflict / is_rate_limited / is_forbidden flags.
GetpayinSignatureErrorA webhook signature did not verify.
GetpayinConnectionErrorNetwork failure or timeout (no HTTP response).
fromgetpayinimportGetpayinApiErrortry:
getpayin.payments.refund(invoice_id=123, amount="10.00")
exceptGetpayinApiErroraserror:
iferror.is_idempotency_conflict:
... # a refund with this idempotency key already exists

Retries and rate limiting

Every integration endpoint is rate limited server-side, so 429s are an expected condition under burst traffic. The SDK retries transient failures — 429, 5xx, connection errors, and timeouts — with exponential backoff and full jitter, honoring the server's Retry-After header when present.

A request is only ever replayed when replaying it cannot double-charge:

ReplayedNot replayed
All GETs (recurring.status)vcc.charge, cards.charge, cards.tokenize
Any call you pass an idempotency_key toinvoices.create, recurring.create without a key
payments.check_status (a pure read)recurring.cancel / pause / resume

So to make a refund safely retryable, pass an idempotency key — otherwise a failed refund surfaces immediately and is yours to handle. Tune or disable retries per client with max_retries (set 0 to turn them off). timeout applies to each attempt, so worst-case wall time is roughly (max_retries + 1) × timeout plus backoff. For requests the SDK will not replay, GetpayinApiError.retry_after_seconds exposes the server's backoff hint.

Amounts and precision

Signatures are computed over the exact bytes sent on the wire. To avoid any floating-point ambiguity, pass monetary amounts as strings (e.g. "10.50"). Numbers are accepted and stringified, but strings give you full control.

Custom transport

The default transport uses the standard library. Inject any callable matching getpayin.Transport — for a proxy-aware or connection-pooled HTTP client, or a mock in tests:

deftransport(method, url, headers, body, timeout):
resp=requests.request(method, url, headers=headers, data=body, timeout=timeout)
returngetpayin.HttpResponse(status=resp.status_code, text=resp.text, headers=dict(resp.headers))
getpayin=GetpayinClient(public_token=..., hash_token=..., transport=transport)

API reference

The full HTTP API — endpoints, fields, error codes, and test cards — is documented in the GetPayIn API reference: https://pay.getpayin.com/docs/payment_integration/index.html

Contributing

See CONTRIBUTING.md — in particular the note on signed-field ordering, which is the one thing that must stay in lockstep with the server.

Security issues: see SECURITY.md. Please do not open a public issue for a vulnerability.

License

MIT

About

Official server-side Python SDK for the PayLink / GetPayIn payment integration API.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

getpayin

CIPyPIPythonLicense

Official server-side Python SDK for the GetPayIn payment integration API. It wraps every integration endpoint with an idiomatic, typed API and computes the order-sensitive HMAC-SHA256 signatures for you, so you never have to build them by hand.

  • Checkouts (invoices.create)
  • Payment operations (payments.void / refund / settle / reverse_authorization / check_status)
  • Server-to-server card charges (vcc.charge)
  • Card tokenization (cards.tokenize / charge / revoke)
  • Recurring mandates (recurring.create / status / cancel / pause / resume)
  • Webhook signature verification (webhooks.verify)

Server-side only. Signing uses your secret hash_token. Never ship it to a browser or mobile client.

Requirements

  • Python 3.8+
  • Zero runtime dependencies — the default transport is built on the standard library

Install

pip install getpayin

Quick start

importosfromgetpayinimportGetpayinClientgetpayin=GetpayinClient(
public_token=os.environ["GETPAYIN_PUBLIC_TOKEN"],
hash_token=os.environ["GETPAYIN_HASH_TOKEN"], # secret — server-side only# base_url defaults to https://pay.getpayin.com# timeout defaults to 30.0 seconds (per attempt)# max_retries defaults to 2 (set 0 to disable retries)
)
checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00", # pass amounts as strings to control the exact wire formcurrency="USD",
redirection_url="https://shop.example.com/return",
webhook_url="https://shop.example.com/webhooks/getpayin",
)
# Redirect the payer to the hosted checkout:print(checkout.checkout_url, checkout.invoice_id, checkout.expires_at)

For an embedded checkout you can render inside your own page, pass iframe=True (sent in the request body but excluded from the signature, like payment_mode):

checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00",
currency="USD",
iframe=True, # enable embedded/iframe checkout
)

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='getpayin_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

Both credentials are issued in the GetPayIn dashboard under Settings → Payment Integrations. public_token is sent on every request; hash_token is the secret used only to sign — it never leaves your server.

Payment operations

getpayin.payments.void(invoice_id=123)
getpayin.payments.settle(invoice_id=123, amount="50.00")
getpayin.payments.reverse_authorization(invoice_id=123)
status=getpayin.payments.check_status(invoice_id=123)
# PaymentResult(invoice_id=123, paid_status='PAID', auth_code='...')# Refunds are idempotent when you pass an idempotency key — safe to retry:refund=getpayin.payments.refund(
invoice_id=123, amount="10.50", idempotency_key="refund-order-1234"
)
# RefundResult(..., refund_amount=10.5)

Card tokenization

result=getpayin.cards.tokenize(
first_name="Jane", last_name="Doe",
card_number="4111111111111111",
card_expiry_month="12", card_expiry_year="2030", card_cvv="123",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.charge(
card_token=result.token,
initiator="merchant",
first_name="Jane", last_name="Doe",
currency="USD", price="100.00", product="Monthly rebill",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.revoke(card_token=result.token)

For US and CA billing addresses, also pass the state fields the API requires: us_state + postal_code (US) or canada_state + postal_code (CA).

Recurring mandates

mandate=getpayin.recurring.create(
first_name="Sam", last_name="Doe", email="sam@example.com",
order_title="Gold subscription",
order_amount="250.00", currency="USD",
cadence_interval="month", cadence_count=1, total_cycles=12,
consent_text="I authorise recurring monthly charges.",
idempotency_key="sub-signup-42",
)
getpayin.recurring.status(mandate.mandate_id)
getpayin.recurring.pause(mandate.mandate_id)
getpayin.recurring.resume(mandate.mandate_id)
getpayin.recurring.cancel(mandate.mandate_id)

Idempotency

Retrying a write after a network error or timeout risks performing it twice. To make that safe, pass an idempotency_key — the SDK sends it as the Idempotency-Key header and the server returns the original result instead of charging, refunding, or creating a second time. Keys are scoped per integration and capped at 64 characters.

MethodA replay with the same key returns
invoices.createthe original invoice and checkout_url
vcc.chargethe original charge
cards.chargethe original charge
payments.refundthe original refund
recurring.createthe original mandate

Reusing a key with a different request — for example recurring.create with changed terms, or payments.refund for a different amount — is rejected as a conflict: a GetpayinApiError with is_idempotency_conflict set (HTTP 409). Only the methods above honor the header.

Verifying webhooks

Pass the parsed body (a dict) or the raw JSON string/bytes to verify. It recomputes the signature with your hash_token and compares in constant time.

fromflaskimportFlask, request, abortfromgetpayinimportGetpayinClient, GetpayinSignatureErrorgetpayin=GetpayinClient(public_token=..., hash_token=...)
app=Flask(__name__)
@app.post("/webhooks/getpayin")defwebhook():
try:
event=getpayin.webhooks.verify(request.get_data(as_text=True))
exceptGetpayinSignatureError:
abort(400)
# event.event, event.invoice_id, event.success, event.raw, ...return"", 200

GetPayIn webhook signatures carry no timestamp, so verification does not protect against replay. Pair it with your own idempotency keyed on invoice_id.

Error handling

Every failure is a subclass of GetpayinError:

ErrorWhen
GetpayinConfigErrorInvalid client configuration (missing tokens, non-positive timeout).
GetpayinApiErrorThe API returned an error. Carries status, errors, raw, retry_after_seconds, and the is_idempotency_conflict / is_rate_limited / is_forbidden flags.
GetpayinSignatureErrorA webhook signature did not verify.
GetpayinConnectionErrorNetwork failure or timeout (no HTTP response).
fromgetpayinimportGetpayinApiErrortry:
getpayin.payments.refund(invoice_id=123, amount="10.00")
exceptGetpayinApiErroraserror:
iferror.is_idempotency_conflict:
... # a refund with this idempotency key already exists

Retries and rate limiting

Every integration endpoint is rate limited server-side, so 429s are an expected condition under burst traffic. The SDK retries transient failures — 429, 5xx, connection errors, and timeouts — with exponential backoff and full jitter, honoring the server's Retry-After header when present.

A request is only ever replayed when replaying it cannot double-charge:

ReplayedNot replayed
All GETs (recurring.status)vcc.charge, cards.charge, cards.tokenize
Any call you pass an idempotency_key toinvoices.create, recurring.create without a key
payments.check_status (a pure read)recurring.cancel / pause / resume

So to make a refund safely retryable, pass an idempotency key — otherwise a failed refund surfaces immediately and is yours to handle. Tune or disable retries per client with max_retries (set 0 to turn them off). timeout applies to each attempt, so worst-case wall time is roughly (max_retries + 1) × timeout plus backoff. For requests the SDK will not replay, GetpayinApiError.retry_after_seconds exposes the server's backoff hint.

Amounts and precision

Signatures are computed over the exact bytes sent on the wire. To avoid any floating-point ambiguity, pass monetary amounts as strings (e.g. "10.50"). Numbers are accepted and stringified, but strings give you full control.

Custom transport

The default transport uses the standard library. Inject any callable matching getpayin.Transport — for a proxy-aware or connection-pooled HTTP client, or a mock in tests:

deftransport(method, url, headers, body, timeout):
resp=requests.request(method, url, headers=headers, data=body, timeout=timeout)
returngetpayin.HttpResponse(status=resp.status_code, text=resp.text, headers=dict(resp.headers))
getpayin=GetpayinClient(public_token=..., hash_token=..., transport=transport)

API reference

The full HTTP API — endpoints, fields, error codes, and test cards — is documented in the GetPayIn API reference: https://pay.getpayin.com/docs/payment_integration/index.html

Contributing

See CONTRIBUTING.md — in particular the note on signed-field ordering, which is the one thing that must stay in lockstep with the server.

Security issues: see SECURITY.md. Please do not open a public issue for a vulnerability.

License

MIT

About

Official server-side Python SDK for the PayLink / GetPayIn payment integration API.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

getpayin

CIPyPIPythonLicense

Official server-side Python SDK for the GetPayIn payment integration API. It wraps every integration endpoint with an idiomatic, typed API and computes the order-sensitive HMAC-SHA256 signatures for you, so you never have to build them by hand.

  • Checkouts (invoices.create)
  • Payment operations (payments.void / refund / settle / reverse_authorization / check_status)
  • Server-to-server card charges (vcc.charge)
  • Card tokenization (cards.tokenize / charge / revoke)
  • Recurring mandates (recurring.create / status / cancel / pause / resume)
  • Webhook signature verification (webhooks.verify)

Server-side only. Signing uses your secret hash_token. Never ship it to a browser or mobile client.

Requirements

  • Python 3.8+
  • Zero runtime dependencies — the default transport is built on the standard library

Install

pip install getpayin

Quick start

importosfromgetpayinimportGetpayinClientgetpayin=GetpayinClient(
public_token=os.environ["GETPAYIN_PUBLIC_TOKEN"],
hash_token=os.environ["GETPAYIN_HASH_TOKEN"], # secret — server-side only# base_url defaults to https://pay.getpayin.com# timeout defaults to 30.0 seconds (per attempt)# max_retries defaults to 2 (set 0 to disable retries)
)
checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00", # pass amounts as strings to control the exact wire formcurrency="USD",
redirection_url="https://shop.example.com/return",
webhook_url="https://shop.example.com/webhooks/getpayin",
)
# Redirect the payer to the hosted checkout:print(checkout.checkout_url, checkout.invoice_id, checkout.expires_at)

For an embedded checkout you can render inside your own page, pass iframe=True (sent in the request body but excluded from the signature, like payment_mode):

checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00",
currency="USD",
iframe=True, # enable embedded/iframe checkout
)

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='getpayin_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

Both credentials are issued in the GetPayIn dashboard under Settings → Payment Integrations. public_token is sent on every request; hash_token is the secret used only to sign — it never leaves your server.

Payment operations

getpayin.payments.void(invoice_id=123)
getpayin.payments.settle(invoice_id=123, amount="50.00")
getpayin.payments.reverse_authorization(invoice_id=123)
status=getpayin.payments.check_status(invoice_id=123)
# PaymentResult(invoice_id=123, paid_status='PAID', auth_code='...')# Refunds are idempotent when you pass an idempotency key — safe to retry:refund=getpayin.payments.refund(
invoice_id=123, amount="10.50", idempotency_key="refund-order-1234"
)
# RefundResult(..., refund_amount=10.5)

Card tokenization

result=getpayin.cards.tokenize(
first_name="Jane", last_name="Doe",
card_number="4111111111111111",
card_expiry_month="12", card_expiry_year="2030", card_cvv="123",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.charge(
card_token=result.token,
initiator="merchant",
first_name="Jane", last_name="Doe",
currency="USD", price="100.00", product="Monthly rebill",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.revoke(card_token=result.token)

For US and CA billing addresses, also pass the state fields the API requires: us_state + postal_code (US) or canada_state + postal_code (CA).

Recurring mandates

mandate=getpayin.recurring.create(
first_name="Sam", last_name="Doe", email="sam@example.com",
order_title="Gold subscription",
order_amount="250.00", currency="USD",
cadence_interval="month", cadence_count=1, total_cycles=12,
consent_text="I authorise recurring monthly charges.",
idempotency_key="sub-signup-42",
)
getpayin.recurring.status(mandate.mandate_id)
getpayin.recurring.pause(mandate.mandate_id)
getpayin.recurring.resume(mandate.mandate_id)
getpayin.recurring.cancel(mandate.mandate_id)

Idempotency

Retrying a write after a network error or timeout risks performing it twice. To make that safe, pass an idempotency_key — the SDK sends it as the Idempotency-Key header and the server returns the original result instead of charging, refunding, or creating a second time. Keys are scoped per integration and capped at 64 characters.

MethodA replay with the same key returns
invoices.createthe original invoice and checkout_url
vcc.chargethe original charge
cards.chargethe original charge
payments.refundthe original refund
recurring.createthe original mandate

Reusing a key with a different request — for example recurring.create with changed terms, or payments.refund for a different amount — is rejected as a conflict: a GetpayinApiError with is_idempotency_conflict set (HTTP 409). Only the methods above honor the header.

Verifying webhooks

Pass the parsed body (a dict) or the raw JSON string/bytes to verify. It recomputes the signature with your hash_token and compares in constant time.

fromflaskimportFlask, request, abortfromgetpayinimportGetpayinClient, GetpayinSignatureErrorgetpayin=GetpayinClient(public_token=..., hash_token=...)
app=Flask(__name__)
@app.post("/webhooks/getpayin")defwebhook():
try:
event=getpayin.webhooks.verify(request.get_data(as_text=True))
exceptGetpayinSignatureError:
abort(400)
# event.event, event.invoice_id, event.success, event.raw, ...return"", 200

GetPayIn webhook signatures carry no timestamp, so verification does not protect against replay. Pair it with your own idempotency keyed on invoice_id.

Error handling

Every failure is a subclass of GetpayinError:

ErrorWhen
GetpayinConfigErrorInvalid client configuration (missing tokens, non-positive timeout).
GetpayinApiErrorThe API returned an error. Carries status, errors, raw, retry_after_seconds, and the is_idempotency_conflict / is_rate_limited / is_forbidden flags.
GetpayinSignatureErrorA webhook signature did not verify.
GetpayinConnectionErrorNetwork failure or timeout (no HTTP response).
fromgetpayinimportGetpayinApiErrortry:
getpayin.payments.refund(invoice_id=123, amount="10.00")
exceptGetpayinApiErroraserror:
iferror.is_idempotency_conflict:
... # a refund with this idempotency key already exists

Retries and rate limiting

Every integration endpoint is rate limited server-side, so 429s are an expected condition under burst traffic. The SDK retries transient failures — 429, 5xx, connection errors, and timeouts — with exponential backoff and full jitter, honoring the server's Retry-After header when present.

A request is only ever replayed when replaying it cannot double-charge:

ReplayedNot replayed
All GETs (recurring.status)vcc.charge, cards.charge, cards.tokenize
Any call you pass an idempotency_key toinvoices.create, recurring.create without a key
payments.check_status (a pure read)recurring.cancel / pause / resume

So to make a refund safely retryable, pass an idempotency key — otherwise a failed refund surfaces immediately and is yours to handle. Tune or disable retries per client with max_retries (set 0 to turn them off). timeout applies to each attempt, so worst-case wall time is roughly (max_retries + 1) × timeout plus backoff. For requests the SDK will not replay, GetpayinApiError.retry_after_seconds exposes the server's backoff hint.

Amounts and precision

Signatures are computed over the exact bytes sent on the wire. To avoid any floating-point ambiguity, pass monetary amounts as strings (e.g. "10.50"). Numbers are accepted and stringified, but strings give you full control.

Custom transport

The default transport uses the standard library. Inject any callable matching getpayin.Transport — for a proxy-aware or connection-pooled HTTP client, or a mock in tests:

deftransport(method, url, headers, body, timeout):
resp=requests.request(method, url, headers=headers, data=body, timeout=timeout)
returngetpayin.HttpResponse(status=resp.status_code, text=resp.text, headers=dict(resp.headers))
getpayin=GetpayinClient(public_token=..., hash_token=..., transport=transport)

API reference

The full HTTP API — endpoints, fields, error codes, and test cards — is documented in the GetPayIn API reference: https://pay.getpayin.com/docs/payment_integration/index.html

Contributing

See CONTRIBUTING.md — in particular the note on signed-field ordering, which is the one thing that must stay in lockstep with the server.

Security issues: see SECURITY.md. Please do not open a public issue for a vulnerability.

License

MIT

About

Official server-side Python SDK for the PayLink / GetPayIn payment integration API.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

getpayin

CIPyPIPythonLicense

Official server-side Python SDK for the GetPayIn payment integration API. It wraps every integration endpoint with an idiomatic, typed API and computes the order-sensitive HMAC-SHA256 signatures for you, so you never have to build them by hand.

  • Checkouts (invoices.create)
  • Payment operations (payments.void / refund / settle / reverse_authorization / check_status)
  • Server-to-server card charges (vcc.charge)
  • Card tokenization (cards.tokenize / charge / revoke)
  • Recurring mandates (recurring.create / status / cancel / pause / resume)
  • Webhook signature verification (webhooks.verify)

Server-side only. Signing uses your secret hash_token. Never ship it to a browser or mobile client.

Requirements

  • Python 3.8+
  • Zero runtime dependencies — the default transport is built on the standard library

Install

pip install getpayin

Quick start

importosfromgetpayinimportGetpayinClientgetpayin=GetpayinClient(
public_token=os.environ["GETPAYIN_PUBLIC_TOKEN"],
hash_token=os.environ["GETPAYIN_HASH_TOKEN"], # secret — server-side only# base_url defaults to https://pay.getpayin.com# timeout defaults to 30.0 seconds (per attempt)# max_retries defaults to 2 (set 0 to disable retries)
)
checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00", # pass amounts as strings to control the exact wire formcurrency="USD",
redirection_url="https://shop.example.com/return",
webhook_url="https://shop.example.com/webhooks/getpayin",
)
# Redirect the payer to the hosted checkout:print(checkout.checkout_url, checkout.invoice_id, checkout.expires_at)

For an embedded checkout you can render inside your own page, pass iframe=True (sent in the request body but excluded from the signature, like payment_mode):

checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00",
currency="USD",
iframe=True, # enable embedded/iframe checkout
)

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='getpayin_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

Both credentials are issued in the GetPayIn dashboard under Settings → Payment Integrations. public_token is sent on every request; hash_token is the secret used only to sign — it never leaves your server.

Payment operations

getpayin.payments.void(invoice_id=123)
getpayin.payments.settle(invoice_id=123, amount="50.00")
getpayin.payments.reverse_authorization(invoice_id=123)
status=getpayin.payments.check_status(invoice_id=123)
# PaymentResult(invoice_id=123, paid_status='PAID', auth_code='...')# Refunds are idempotent when you pass an idempotency key — safe to retry:refund=getpayin.payments.refund(
invoice_id=123, amount="10.50", idempotency_key="refund-order-1234"
)
# RefundResult(..., refund_amount=10.5)

Card tokenization

result=getpayin.cards.tokenize(
first_name="Jane", last_name="Doe",
card_number="4111111111111111",
card_expiry_month="12", card_expiry_year="2030", card_cvv="123",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.charge(
card_token=result.token,
initiator="merchant",
first_name="Jane", last_name="Doe",
currency="USD", price="100.00", product="Monthly rebill",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.revoke(card_token=result.token)

For US and CA billing addresses, also pass the state fields the API requires: us_state + postal_code (US) or canada_state + postal_code (CA).

Recurring mandates

mandate=getpayin.recurring.create(
first_name="Sam", last_name="Doe", email="sam@example.com",
order_title="Gold subscription",
order_amount="250.00", currency="USD",
cadence_interval="month", cadence_count=1, total_cycles=12,
consent_text="I authorise recurring monthly charges.",
idempotency_key="sub-signup-42",
)
getpayin.recurring.status(mandate.mandate_id)
getpayin.recurring.pause(mandate.mandate_id)
getpayin.recurring.resume(mandate.mandate_id)
getpayin.recurring.cancel(mandate.mandate_id)

Idempotency

Retrying a write after a network error or timeout risks performing it twice. To make that safe, pass an idempotency_key — the SDK sends it as the Idempotency-Key header and the server returns the original result instead of charging, refunding, or creating a second time. Keys are scoped per integration and capped at 64 characters.

MethodA replay with the same key returns
invoices.createthe original invoice and checkout_url
vcc.chargethe original charge
cards.chargethe original charge
payments.refundthe original refund
recurring.createthe original mandate

Reusing a key with a different request — for example recurring.create with changed terms, or payments.refund for a different amount — is rejected as a conflict: a GetpayinApiError with is_idempotency_conflict set (HTTP 409). Only the methods above honor the header.

Verifying webhooks

Pass the parsed body (a dict) or the raw JSON string/bytes to verify. It recomputes the signature with your hash_token and compares in constant time.

fromflaskimportFlask, request, abortfromgetpayinimportGetpayinClient, GetpayinSignatureErrorgetpayin=GetpayinClient(public_token=..., hash_token=...)
app=Flask(__name__)
@app.post("/webhooks/getpayin")defwebhook():
try:
event=getpayin.webhooks.verify(request.get_data(as_text=True))
exceptGetpayinSignatureError:
abort(400)
# event.event, event.invoice_id, event.success, event.raw, ...return"", 200

GetPayIn webhook signatures carry no timestamp, so verification does not protect against replay. Pair it with your own idempotency keyed on invoice_id.

Error handling

Every failure is a subclass of GetpayinError:

ErrorWhen
GetpayinConfigErrorInvalid client configuration (missing tokens, non-positive timeout).
GetpayinApiErrorThe API returned an error. Carries status, errors, raw, retry_after_seconds, and the is_idempotency_conflict / is_rate_limited / is_forbidden flags.
GetpayinSignatureErrorA webhook signature did not verify.
GetpayinConnectionErrorNetwork failure or timeout (no HTTP response).
fromgetpayinimportGetpayinApiErrortry:
getpayin.payments.refund(invoice_id=123, amount="10.00")
exceptGetpayinApiErroraserror:
iferror.is_idempotency_conflict:
... # a refund with this idempotency key already exists

Retries and rate limiting

Every integration endpoint is rate limited server-side, so 429s are an expected condition under burst traffic. The SDK retries transient failures — 429, 5xx, connection errors, and timeouts — with exponential backoff and full jitter, honoring the server's Retry-After header when present.

A request is only ever replayed when replaying it cannot double-charge:

ReplayedNot replayed
All GETs (recurring.status)vcc.charge, cards.charge, cards.tokenize
Any call you pass an idempotency_key toinvoices.create, recurring.create without a key
payments.check_status (a pure read)recurring.cancel / pause / resume

So to make a refund safely retryable, pass an idempotency key — otherwise a failed refund surfaces immediately and is yours to handle. Tune or disable retries per client with max_retries (set 0 to turn them off). timeout applies to each attempt, so worst-case wall time is roughly (max_retries + 1) × timeout plus backoff. For requests the SDK will not replay, GetpayinApiError.retry_after_seconds exposes the server's backoff hint.

Amounts and precision

Signatures are computed over the exact bytes sent on the wire. To avoid any floating-point ambiguity, pass monetary amounts as strings (e.g. "10.50"). Numbers are accepted and stringified, but strings give you full control.

Custom transport

The default transport uses the standard library. Inject any callable matching getpayin.Transport — for a proxy-aware or connection-pooled HTTP client, or a mock in tests:

deftransport(method, url, headers, body, timeout):
resp=requests.request(method, url, headers=headers, data=body, timeout=timeout)
returngetpayin.HttpResponse(status=resp.status_code, text=resp.text, headers=dict(resp.headers))
getpayin=GetpayinClient(public_token=..., hash_token=..., transport=transport)

API reference

The full HTTP API — endpoints, fields, error codes, and test cards — is documented in the GetPayIn API reference: https://pay.getpayin.com/docs/payment_integration/index.html

Contributing

See CONTRIBUTING.md — in particular the note on signed-field ordering, which is the one thing that must stay in lockstep with the server.

Security issues: see SECURITY.md. Please do not open a public issue for a vulnerability.

License

MIT

About

Official server-side Python SDK for the PayLink / GetPayIn payment integration API.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

getpayin

CIPyPIPythonLicense

Official server-side Python SDK for the GetPayIn payment integration API. It wraps every integration endpoint with an idiomatic, typed API and computes the order-sensitive HMAC-SHA256 signatures for you, so you never have to build them by hand.

  • Checkouts (invoices.create)
  • Payment operations (payments.void / refund / settle / reverse_authorization / check_status)
  • Server-to-server card charges (vcc.charge)
  • Card tokenization (cards.tokenize / charge / revoke)
  • Recurring mandates (recurring.create / status / cancel / pause / resume)
  • Webhook signature verification (webhooks.verify)

Server-side only. Signing uses your secret hash_token. Never ship it to a browser or mobile client.

Requirements

  • Python 3.8+
  • Zero runtime dependencies — the default transport is built on the standard library

Install

pip install getpayin

Quick start

importosfromgetpayinimportGetpayinClientgetpayin=GetpayinClient(
public_token=os.environ["GETPAYIN_PUBLIC_TOKEN"],
hash_token=os.environ["GETPAYIN_HASH_TOKEN"], # secret — server-side only# base_url defaults to https://pay.getpayin.com# timeout defaults to 30.0 seconds (per attempt)# max_retries defaults to 2 (set 0 to disable retries)
)
checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00", # pass amounts as strings to control the exact wire formcurrency="USD",
redirection_url="https://shop.example.com/return",
webhook_url="https://shop.example.com/webhooks/getpayin",
)
# Redirect the payer to the hosted checkout:print(checkout.checkout_url, checkout.invoice_id, checkout.expires_at)

For an embedded checkout you can render inside your own page, pass iframe=True (sent in the request body but excluded from the signature, like payment_mode):

checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00",
currency="USD",
iframe=True, # enable embedded/iframe checkout
)

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='getpayin_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

Both credentials are issued in the GetPayIn dashboard under Settings → Payment Integrations. public_token is sent on every request; hash_token is the secret used only to sign — it never leaves your server.

Payment operations

getpayin.payments.void(invoice_id=123)
getpayin.payments.settle(invoice_id=123, amount="50.00")
getpayin.payments.reverse_authorization(invoice_id=123)
status=getpayin.payments.check_status(invoice_id=123)
# PaymentResult(invoice_id=123, paid_status='PAID', auth_code='...')# Refunds are idempotent when you pass an idempotency key — safe to retry:refund=getpayin.payments.refund(
invoice_id=123, amount="10.50", idempotency_key="refund-order-1234"
)
# RefundResult(..., refund_amount=10.5)

Card tokenization

result=getpayin.cards.tokenize(
first_name="Jane", last_name="Doe",
card_number="4111111111111111",
card_expiry_month="12", card_expiry_year="2030", card_cvv="123",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.charge(
card_token=result.token,
initiator="merchant",
first_name="Jane", last_name="Doe",
currency="USD", price="100.00", product="Monthly rebill",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.revoke(card_token=result.token)

For US and CA billing addresses, also pass the state fields the API requires: us_state + postal_code (US) or canada_state + postal_code (CA).

Recurring mandates

mandate=getpayin.recurring.create(
first_name="Sam", last_name="Doe", email="sam@example.com",
order_title="Gold subscription",
order_amount="250.00", currency="USD",
cadence_interval="month", cadence_count=1, total_cycles=12,
consent_text="I authorise recurring monthly charges.",
idempotency_key="sub-signup-42",
)
getpayin.recurring.status(mandate.mandate_id)
getpayin.recurring.pause(mandate.mandate_id)
getpayin.recurring.resume(mandate.mandate_id)
getpayin.recurring.cancel(mandate.mandate_id)

Idempotency

Retrying a write after a network error or timeout risks performing it twice. To make that safe, pass an idempotency_key — the SDK sends it as the Idempotency-Key header and the server returns the original result instead of charging, refunding, or creating a second time. Keys are scoped per integration and capped at 64 characters.

MethodA replay with the same key returns
invoices.createthe original invoice and checkout_url
vcc.chargethe original charge
cards.chargethe original charge
payments.refundthe original refund
recurring.createthe original mandate

Reusing a key with a different request — for example recurring.create with changed terms, or payments.refund for a different amount — is rejected as a conflict: a GetpayinApiError with is_idempotency_conflict set (HTTP 409). Only the methods above honor the header.

Verifying webhooks

Pass the parsed body (a dict) or the raw JSON string/bytes to verify. It recomputes the signature with your hash_token and compares in constant time.

fromflaskimportFlask, request, abortfromgetpayinimportGetpayinClient, GetpayinSignatureErrorgetpayin=GetpayinClient(public_token=..., hash_token=...)
app=Flask(__name__)
@app.post("/webhooks/getpayin")defwebhook():
try:
event=getpayin.webhooks.verify(request.get_data(as_text=True))
exceptGetpayinSignatureError:
abort(400)
# event.event, event.invoice_id, event.success, event.raw, ...return"", 200

GetPayIn webhook signatures carry no timestamp, so verification does not protect against replay. Pair it with your own idempotency keyed on invoice_id.

Error handling

Every failure is a subclass of GetpayinError:

ErrorWhen
GetpayinConfigErrorInvalid client configuration (missing tokens, non-positive timeout).
GetpayinApiErrorThe API returned an error. Carries status, errors, raw, retry_after_seconds, and the is_idempotency_conflict / is_rate_limited / is_forbidden flags.
GetpayinSignatureErrorA webhook signature did not verify.
GetpayinConnectionErrorNetwork failure or timeout (no HTTP response).
fromgetpayinimportGetpayinApiErrortry:
getpayin.payments.refund(invoice_id=123, amount="10.00")
exceptGetpayinApiErroraserror:
iferror.is_idempotency_conflict:
... # a refund with this idempotency key already exists

Retries and rate limiting

Every integration endpoint is rate limited server-side, so 429s are an expected condition under burst traffic. The SDK retries transient failures — 429, 5xx, connection errors, and timeouts — with exponential backoff and full jitter, honoring the server's Retry-After header when present.

A request is only ever replayed when replaying it cannot double-charge:

ReplayedNot replayed
All GETs (recurring.status)vcc.charge, cards.charge, cards.tokenize
Any call you pass an idempotency_key toinvoices.create, recurring.create without a key
payments.check_status (a pure read)recurring.cancel / pause / resume

So to make a refund safely retryable, pass an idempotency key — otherwise a failed refund surfaces immediately and is yours to handle. Tune or disable retries per client with max_retries (set 0 to turn them off). timeout applies to each attempt, so worst-case wall time is roughly (max_retries + 1) × timeout plus backoff. For requests the SDK will not replay, GetpayinApiError.retry_after_seconds exposes the server's backoff hint.

Amounts and precision

Signatures are computed over the exact bytes sent on the wire. To avoid any floating-point ambiguity, pass monetary amounts as strings (e.g. "10.50"). Numbers are accepted and stringified, but strings give you full control.

Custom transport

The default transport uses the standard library. Inject any callable matching getpayin.Transport — for a proxy-aware or connection-pooled HTTP client, or a mock in tests:

deftransport(method, url, headers, body, timeout):
resp=requests.request(method, url, headers=headers, data=body, timeout=timeout)
returngetpayin.HttpResponse(status=resp.status_code, text=resp.text, headers=dict(resp.headers))
getpayin=GetpayinClient(public_token=..., hash_token=..., transport=transport)

API reference

The full HTTP API — endpoints, fields, error codes, and test cards — is documented in the GetPayIn API reference: https://pay.getpayin.com/docs/payment_integration/index.html

Contributing

See CONTRIBUTING.md — in particular the note on signed-field ordering, which is the one thing that must stay in lockstep with the server.

Security issues: see SECURITY.md. Please do not open a public issue for a vulnerability.

License

MIT

About

Official server-side Python SDK for the PayLink / GetPayIn payment integration API.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

getpayin

CIPyPIPythonLicense

Official server-side Python SDK for the GetPayIn payment integration API. It wraps every integration endpoint with an idiomatic, typed API and computes the order-sensitive HMAC-SHA256 signatures for you, so you never have to build them by hand.

  • Checkouts (invoices.create)
  • Payment operations (payments.void / refund / settle / reverse_authorization / check_status)
  • Server-to-server card charges (vcc.charge)
  • Card tokenization (cards.tokenize / charge / revoke)
  • Recurring mandates (recurring.create / status / cancel / pause / resume)
  • Webhook signature verification (webhooks.verify)

Server-side only. Signing uses your secret hash_token. Never ship it to a browser or mobile client.

Requirements

  • Python 3.8+
  • Zero runtime dependencies — the default transport is built on the standard library

Install

pip install getpayin

Quick start

importosfromgetpayinimportGetpayinClientgetpayin=GetpayinClient(
public_token=os.environ["GETPAYIN_PUBLIC_TOKEN"],
hash_token=os.environ["GETPAYIN_HASH_TOKEN"], # secret — server-side only# base_url defaults to https://pay.getpayin.com# timeout defaults to 30.0 seconds (per attempt)# max_retries defaults to 2 (set 0 to disable retries)
)
checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00", # pass amounts as strings to control the exact wire formcurrency="USD",
redirection_url="https://shop.example.com/return",
webhook_url="https://shop.example.com/webhooks/getpayin",
)
# Redirect the payer to the hosted checkout:print(checkout.checkout_url, checkout.invoice_id, checkout.expires_at)

For an embedded checkout you can render inside your own page, pass iframe=True (sent in the request body but excluded from the signature, like payment_mode):

checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00",
currency="USD",
iframe=True, # enable embedded/iframe checkout
)

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='getpayin_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

Both credentials are issued in the GetPayIn dashboard under Settings → Payment Integrations. public_token is sent on every request; hash_token is the secret used only to sign — it never leaves your server.

Payment operations

getpayin.payments.void(invoice_id=123)
getpayin.payments.settle(invoice_id=123, amount="50.00")
getpayin.payments.reverse_authorization(invoice_id=123)
status=getpayin.payments.check_status(invoice_id=123)
# PaymentResult(invoice_id=123, paid_status='PAID', auth_code='...')# Refunds are idempotent when you pass an idempotency key — safe to retry:refund=getpayin.payments.refund(
invoice_id=123, amount="10.50", idempotency_key="refund-order-1234"
)
# RefundResult(..., refund_amount=10.5)

Card tokenization

result=getpayin.cards.tokenize(
first_name="Jane", last_name="Doe",
card_number="4111111111111111",
card_expiry_month="12", card_expiry_year="2030", card_cvv="123",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.charge(
card_token=result.token,
initiator="merchant",
first_name="Jane", last_name="Doe",
currency="USD", price="100.00", product="Monthly rebill",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.revoke(card_token=result.token)

For US and CA billing addresses, also pass the state fields the API requires: us_state + postal_code (US) or canada_state + postal_code (CA).

Recurring mandates

mandate=getpayin.recurring.create(
first_name="Sam", last_name="Doe", email="sam@example.com",
order_title="Gold subscription",
order_amount="250.00", currency="USD",
cadence_interval="month", cadence_count=1, total_cycles=12,
consent_text="I authorise recurring monthly charges.",
idempotency_key="sub-signup-42",
)
getpayin.recurring.status(mandate.mandate_id)
getpayin.recurring.pause(mandate.mandate_id)
getpayin.recurring.resume(mandate.mandate_id)
getpayin.recurring.cancel(mandate.mandate_id)

Idempotency

Retrying a write after a network error or timeout risks performing it twice. To make that safe, pass an idempotency_key — the SDK sends it as the Idempotency-Key header and the server returns the original result instead of charging, refunding, or creating a second time. Keys are scoped per integration and capped at 64 characters.

MethodA replay with the same key returns
invoices.createthe original invoice and checkout_url
vcc.chargethe original charge
cards.chargethe original charge
payments.refundthe original refund
recurring.createthe original mandate

Reusing a key with a different request — for example recurring.create with changed terms, or payments.refund for a different amount — is rejected as a conflict: a GetpayinApiError with is_idempotency_conflict set (HTTP 409). Only the methods above honor the header.

Verifying webhooks

Pass the parsed body (a dict) or the raw JSON string/bytes to verify. It recomputes the signature with your hash_token and compares in constant time.

fromflaskimportFlask, request, abortfromgetpayinimportGetpayinClient, GetpayinSignatureErrorgetpayin=GetpayinClient(public_token=..., hash_token=...)
app=Flask(__name__)
@app.post("/webhooks/getpayin")defwebhook():
try:
event=getpayin.webhooks.verify(request.get_data(as_text=True))
exceptGetpayinSignatureError:
abort(400)
# event.event, event.invoice_id, event.success, event.raw, ...return"", 200

GetPayIn webhook signatures carry no timestamp, so verification does not protect against replay. Pair it with your own idempotency keyed on invoice_id.

Error handling

Every failure is a subclass of GetpayinError:

ErrorWhen
GetpayinConfigErrorInvalid client configuration (missing tokens, non-positive timeout).
GetpayinApiErrorThe API returned an error. Carries status, errors, raw, retry_after_seconds, and the is_idempotency_conflict / is_rate_limited / is_forbidden flags.
GetpayinSignatureErrorA webhook signature did not verify.
GetpayinConnectionErrorNetwork failure or timeout (no HTTP response).
fromgetpayinimportGetpayinApiErrortry:
getpayin.payments.refund(invoice_id=123, amount="10.00")
exceptGetpayinApiErroraserror:
iferror.is_idempotency_conflict:
... # a refund with this idempotency key already exists

Retries and rate limiting

Every integration endpoint is rate limited server-side, so 429s are an expected condition under burst traffic. The SDK retries transient failures — 429, 5xx, connection errors, and timeouts — with exponential backoff and full jitter, honoring the server's Retry-After header when present.

A request is only ever replayed when replaying it cannot double-charge:

ReplayedNot replayed
All GETs (recurring.status)vcc.charge, cards.charge, cards.tokenize
Any call you pass an idempotency_key toinvoices.create, recurring.create without a key
payments.check_status (a pure read)recurring.cancel / pause / resume

So to make a refund safely retryable, pass an idempotency key — otherwise a failed refund surfaces immediately and is yours to handle. Tune or disable retries per client with max_retries (set 0 to turn them off). timeout applies to each attempt, so worst-case wall time is roughly (max_retries + 1) × timeout plus backoff. For requests the SDK will not replay, GetpayinApiError.retry_after_seconds exposes the server's backoff hint.

Amounts and precision

Signatures are computed over the exact bytes sent on the wire. To avoid any floating-point ambiguity, pass monetary amounts as strings (e.g. "10.50"). Numbers are accepted and stringified, but strings give you full control.

Custom transport

The default transport uses the standard library. Inject any callable matching getpayin.Transport — for a proxy-aware or connection-pooled HTTP client, or a mock in tests:

deftransport(method, url, headers, body, timeout):
resp=requests.request(method, url, headers=headers, data=body, timeout=timeout)
returngetpayin.HttpResponse(status=resp.status_code, text=resp.text, headers=dict(resp.headers))
getpayin=GetpayinClient(public_token=..., hash_token=..., transport=transport)

API reference

The full HTTP API — endpoints, fields, error codes, and test cards — is documented in the GetPayIn API reference: https://pay.getpayin.com/docs/payment_integration/index.html

Contributing

See CONTRIBUTING.md — in particular the note on signed-field ordering, which is the one thing that must stay in lockstep with the server.

Security issues: see SECURITY.md. Please do not open a public issue for a vulnerability.

License

MIT

About

Official server-side Python SDK for the PayLink / GetPayIn payment integration API.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

getpayin

CIPyPIPythonLicense

Official server-side Python SDK for the GetPayIn payment integration API. It wraps every integration endpoint with an idiomatic, typed API and computes the order-sensitive HMAC-SHA256 signatures for you, so you never have to build them by hand.

  • Checkouts (invoices.create)
  • Payment operations (payments.void / refund / settle / reverse_authorization / check_status)
  • Server-to-server card charges (vcc.charge)
  • Card tokenization (cards.tokenize / charge / revoke)
  • Recurring mandates (recurring.create / status / cancel / pause / resume)
  • Webhook signature verification (webhooks.verify)

Server-side only. Signing uses your secret hash_token. Never ship it to a browser or mobile client.

Requirements

  • Python 3.8+
  • Zero runtime dependencies — the default transport is built on the standard library

Install

pip install getpayin

Quick start

importosfromgetpayinimportGetpayinClientgetpayin=GetpayinClient(
public_token=os.environ["GETPAYIN_PUBLIC_TOKEN"],
hash_token=os.environ["GETPAYIN_HASH_TOKEN"], # secret — server-side only# base_url defaults to https://pay.getpayin.com# timeout defaults to 30.0 seconds (per attempt)# max_retries defaults to 2 (set 0 to disable retries)
)
checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00", # pass amounts as strings to control the exact wire formcurrency="USD",
redirection_url="https://shop.example.com/return",
webhook_url="https://shop.example.com/webhooks/getpayin",
)
# Redirect the payer to the hosted checkout:print(checkout.checkout_url, checkout.invoice_id, checkout.expires_at)

For an embedded checkout you can render inside your own page, pass iframe=True (sent in the request body but excluded from the signature, like payment_mode):

checkout=getpayin.invoices.create(
first_name="John",
last_name="Doe",
email="john@example.com",
order_title="Gold Plan",
order_amount="250.00",
currency="USD",
iframe=True, # enable embedded/iframe checkout
)

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='getpayin_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

Both credentials are issued in the GetPayIn dashboard under Settings → Payment Integrations. public_token is sent on every request; hash_token is the secret used only to sign — it never leaves your server.

Payment operations

getpayin.payments.void(invoice_id=123)
getpayin.payments.settle(invoice_id=123, amount="50.00")
getpayin.payments.reverse_authorization(invoice_id=123)
status=getpayin.payments.check_status(invoice_id=123)
# PaymentResult(invoice_id=123, paid_status='PAID', auth_code='...')# Refunds are idempotent when you pass an idempotency key — safe to retry:refund=getpayin.payments.refund(
invoice_id=123, amount="10.50", idempotency_key="refund-order-1234"
)
# RefundResult(..., refund_amount=10.5)

Card tokenization

result=getpayin.cards.tokenize(
first_name="Jane", last_name="Doe",
card_number="4111111111111111",
card_expiry_month="12", card_expiry_year="2030", card_cvv="123",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.charge(
card_token=result.token,
initiator="merchant",
first_name="Jane", last_name="Doe",
currency="USD", price="100.00", product="Monthly rebill",
country="EG", address="1 Main St", city="Cairo",
)
getpayin.cards.revoke(card_token=result.token)

For US and CA billing addresses, also pass the state fields the API requires: us_state + postal_code (US) or canada_state + postal_code (CA).

Recurring mandates

mandate=getpayin.recurring.create(
first_name="Sam", last_name="Doe", email="sam@example.com",
order_title="Gold subscription",
order_amount="250.00", currency="USD",
cadence_interval="month", cadence_count=1, total_cycles=12,
consent_text="I authorise recurring monthly charges.",
idempotency_key="sub-signup-42",
)
getpayin.recurring.status(mandate.mandate_id)
getpayin.recurring.pause(mandate.mandate_id)
getpayin.recurring.resume(mandate.mandate_id)
getpayin.recurring.cancel(mandate.mandate_id)

Idempotency

Retrying a write after a network error or timeout risks performing it twice. To make that safe, pass an idempotency_key — the SDK sends it as the Idempotency-Key header and the server returns the original result instead of charging, refunding, or creating a second time. Keys are scoped per integration and capped at 64 characters.

MethodA replay with the same key returns
invoices.createthe original invoice and checkout_url
vcc.chargethe original charge
cards.chargethe original charge
payments.refundthe original refund
recurring.createthe original mandate

Reusing a key with a different request — for example recurring.create with changed terms, or payments.refund for a different amount — is rejected as a conflict: a GetpayinApiError with is_idempotency_conflict set (HTTP 409). Only the methods above honor the header.

Verifying webhooks

Pass the parsed body (a dict) or the raw JSON string/bytes to verify. It recomputes the signature with your hash_token and compares in constant time.

fromflaskimportFlask, request, abortfromgetpayinimportGetpayinClient, GetpayinSignatureErrorgetpayin=GetpayinClient(public_token=..., hash_token=...)
app=Flask(__name__)
@app.post("/webhooks/getpayin")defwebhook():
try:
event=getpayin.webhooks.verify(request.get_data(as_text=True))
exceptGetpayinSignatureError:
abort(400)
# event.event, event.invoice_id, event.success, event.raw, ...return"", 200

GetPayIn webhook signatures carry no timestamp, so verification does not protect against replay. Pair it with your own idempotency keyed on invoice_id.

Error handling

Every failure is a subclass of GetpayinError:

ErrorWhen
GetpayinConfigErrorInvalid client configuration (missing tokens, non-positive timeout).
GetpayinApiErrorThe API returned an error. Carries status, errors, raw, retry_after_seconds, and the is_idempotency_conflict / is_rate_limited / is_forbidden flags.
GetpayinSignatureErrorA webhook signature did not verify.
GetpayinConnectionErrorNetwork failure or timeout (no HTTP response).
fromgetpayinimportGetpayinApiErrortry:
getpayin.payments.refund(invoice_id=123, amount="10.00")
exceptGetpayinApiErroraserror:
iferror.is_idempotency_conflict:
... # a refund with this idempotency key already exists

Retries and rate limiting

Every integration endpoint is rate limited server-side, so 429s are an expected condition under burst traffic. The SDK retries transient failures — 429, 5xx, connection errors, and timeouts — with exponential backoff and full jitter, honoring the server's Retry-After header when present.

A request is only ever replayed when replaying it cannot double-charge:

ReplayedNot replayed
All GETs (recurring.status)vcc.charge, cards.charge, cards.tokenize
Any call you pass an idempotency_key toinvoices.create, recurring.create without a key
payments.check_status (a pure read)recurring.cancel / pause / resume

So to make a refund safely retryable, pass an idempotency key — otherwise a failed refund surfaces immediately and is yours to handle. Tune or disable retries per client with max_retries (set 0 to turn them off). timeout applies to each attempt, so worst-case wall time is roughly (max_retries + 1) × timeout plus backoff. For requests the SDK will not replay, GetpayinApiError.retry_after_seconds exposes the server's backoff hint.

Amounts and precision

Signatures are computed over the exact bytes sent on the wire. To avoid any floating-point ambiguity, pass monetary amounts as strings (e.g. "10.50"). Numbers are accepted and stringified, but strings give you full control.

Custom transport

The default transport uses the standard library. Inject any callable matching getpayin.Transport — for a proxy-aware or connection-pooled HTTP client, or a mock in tests:

deftransport(method, url, headers, body, timeout):
resp=requests.request(method, url, headers=headers, data=body, timeout=timeout)
returngetpayin.HttpResponse(status=resp.status_code, text=resp.text, headers=dict(resp.headers))
getpayin=GetpayinClient(public_token=..., hash_token=..., transport=transport)

API reference

The full HTTP API — endpoints, fields, error codes, and test cards — is documented in the GetPayIn API reference: https://pay.getpayin.com/docs/payment_integration/index.html

Contributing

See CONTRIBUTING.md — in particular the note on signed-field ordering, which is the one thing that must stay in lockstep with the server.

Security issues: see SECURITY.md. Please do not open a public issue for a vulnerability.

License

MIT

About

Official server-side Python SDK for the PayLink / GetPayIn payment integration API.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages