Developer Documentation
Complete guide to integrating Magpie Components into your application. Learn how to securely collect payment card data using iframe-based elements.
Magpie Components — Developer Documentation
Table of Contents
- Overview
- How It Works
- Prerequisites
- Quick Start
- SDK Reference
- Element Types
- Styling
- createSource()
- Event Handling
- postMessage Protocol
- API Reference
- Origin Allowlists
- Local Development
- Production Setup
- Security Model
- Error Handling & Troubleshooting
- Full Working Example
1. Overview
Magpie Components is a secure, iframe-based card input system for collecting payment card data without exposing sensitive information to your application. It is made up of three parts:
- SDK (
magpie.js) — A JavaScript library you load on your page. It creates and manages iframes and exposes a simple API for your code to call. - Components — Lightweight HTML pages served from
https://components.magpie.imthat run inside the iframes. They render the actual card input fields and handle all card data directly. - Proxy API — A Laravel backend at
https://components.magpie.im/api/v2that receives card data from the components and forwards it securely to the Magpie API using the merchant's own public key.
Card data (number, expiry, CVC) is entered directly inside the iframes. It never touches your JavaScript, your DOM, or your server. Only a source token (src_xxx) is returned to your page.
2. How It Works
Your Page
└─ Loads magpie.js
└─ Creates three iframes (cardNumber, cardExpiry, cardCvc)
└─ Each iframe loads from https://components.magpie.im
└─ User types card data inside iframes
└─ On createSource():
└─ iframe POSTs card data to /api/v2/sources
└─ Proxy forwards to api.magpie.im with merchant key
└─ Returns source token (src_xxx) to your page
Why iframes? The browser's same-origin policy means your JavaScript cannot read the contents of a cross-origin iframe. Card data typed into the iframes is invisible to your page — only the non-sensitive source token is returned.
Key Design Decisions
- Each card field is a separate iframe (number, expiry, CVC).
- The three iframes synchronize their state using the browser's
BroadcastChannelAPI. - Communication between your page and the iframes uses
window.postMessagewith strict origin validation on both sides. - The merchant's API key is sent from your page to the iframe via
postMessageduring initialization. It is never sent to your server.
3. Prerequisites
- A Magpie public API key (
pk_live_...orpk_test_...). - Your site must be served over HTTPS in production.
- Your origin must be on the Magpie Components allowlist (see Section 12).
- Modern browser support: Chrome, Firefox, Safari, Edge (BroadcastChannel API required).
4. Quick Start
Step 1 — Add the SDK script to your page.
<script src="https://components.magpie.im/sdk/magpie.js"></script>
Step 2 — Add container elements for each card field.
<div id="card-number"></div>
<div id="card-expiry"></div>
<div id="card-cvc"></div>
Step 3 — Initialize the SDK and mount the elements.
const magpie = new Magpie("pk_live_your_key");
const elements = magpie.elements();
const cardNumber = elements.create("cardNumber");
const cardExpiry = elements.create("cardExpiry");
const cardCvc = elements.create("cardCvc");
cardNumber.mount("#card-number");
cardExpiry.mount("#card-expiry");
cardCvc.mount("#card-cvc");
Step 4 — Call createSource() when the user submits.
document.querySelector("#pay").addEventListener("click", async () => {
try {
const source = await cardNumber.createSource({
name: "Cardholder Name", // Required
redirect: {
success: "https://your-site.com/checkout/success",
fail: "https://your-site.com/checkout/fail",
notify: "https://your-site.com/checkout/notify"
}
});
// source.id is the token — send it to your server
console.log("Source created:", source.id);
} catch (err) {
console.error("Error:", err.message);
}
});
That's it. Your page receives a source.id which you send to your server to complete the charge.
5. SDK Reference
Magpie
The root class. Instantiate once per page.
const magpie = new Magpie(publicKey, options);
| Parameter | Type | Required | Description |
|---|---|---|---|
publicKey |
string |
Yes | Your Magpie public API key (pk_live_... or pk_test_...) |
options.componentsUrl |
string |
No | Override the components base URL. Defaults to https://components.magpie.im |
Example:
// Production
const magpie = new Magpie("pk_live_abc123");
// Custom components URL (e.g. local development)
const magpie = new Magpie("pk_test_abc123", {
componentsUrl: "https://elements-dev.your-domain.com"
});
Elements
A factory for creating individual card field elements. Obtain it by calling magpie.elements().
const elements = magpie.elements();
elements.create(type, options)
Creates a single card field element.
| Parameter | Type | Required | Description |
|---|---|---|---|
type |
string |
Yes | One of: "cardNumber", "cardExpiry", "cardCvc" |
options.style |
object |
No | CSS style overrides applied inside the iframe (see Section 7) |
Returns an Element instance.
Element
Represents a single card input field. The main methods you will use are mount() and createSource().
element.mount(selector)
Mounts the iframe into a DOM element. Call once per element after the DOM is ready.
cardNumber.mount("#card-number");
| Parameter | Type | Description |
|---|---|---|
selector |
string |
A CSS selector for the container element |
The iframe is sized to fill the container. Style the container yourself (border, padding, border-radius, etc.).
element.createSource(data)
Triggers card data collection from all three fields and submits to the Magpie API. Returns a Promise that resolves with the source object or rejects with an error.
const source = await cardNumber.createSource(data);
See Section 8 for full parameter and response documentation.
Note: Call
createSource()on any one of the three element instances — it does not matter which. All three iframes share state viaBroadcastChannel.
6. Element Types
| Type | Field | Validation | Auto-format |
|---|---|---|---|
cardNumber |
Card number | Luhn algorithm, min 16 digits | Groups of 4 digits with spaces |
cardExpiry |
Expiry date | Valid month (1–12), future date | MM / YY |
cardCvc |
CVC / CVV | 3–4 digits | Digits only |
Auto-focus behavior: When the card number field is complete, focus moves automatically to the expiry field. When expiry is complete, focus moves to the CVC field. This is handled by the SDK automatically — you do not need to wire it up.
Brand detection: The card number field detects and displays the card brand icon as the user types. Supported brands: Visa, Mastercard, JCB, Amex, Discover.
7. Styling
Pass a style object when calling elements.create(). Styles are applied inside the iframe to the <input> element.
const cardNumber = elements.create("cardNumber", {
style: {
base: {
fontFamily: "Inter, system-ui, sans-serif",
fontSize: "16px",
fontWeight: "400",
color: "#111827",
"::placeholder": {
color: "#9CA3AF"
}
}
}
});
Supported style properties:
| Property | Example |
|---|---|
fontFamily |
"Inter, sans-serif" |
fontSize |
"16px" |
fontWeight |
"400" |
color |
"#111827" |
::placeholder |
{ color: "#9CA3AF" } |
Container styling (the element that holds the iframe) is done with regular CSS on your page:
.field {
border: 1px solid #D1D5DB;
border-radius: 6px;
padding: 10px 12px;
height: 44px;
box-sizing: border-box;
}
8. createSource()
The card number, expiry, and CVC are collected automatically from the mounted fields. Everything else — cardholder details, billing/shipping info, and redirect URLs — is passed as an argument to createSource().
Minimal call
The cardholder name and redirect URLs (success and fail) are required:
const source = await cardNumber.createSource({
name: "Cardholder Name",
redirect: {
success: "https://your-site.com/success",
fail: "https://your-site.com/fail",
notify: "https://your-site.com/notify" // Optional
}
});
Complete call
const source = await cardNumber.createSource({
name: "Gerry Isaac",
address_line1: "#123 JP Rizal St.",
address_line2: "Brgy. Aguinaldo",
address_city: "Quezon City",
address_state: "Metro Manila",
address_country: "PH",
address_zip: "1100",
redirect: {
success: "https://your-site.com/checkout/success",
fail: "https://your-site.com/checkout/fail",
notify: "https://your-site.com/checkout/notify"
},
owner: {
name: "Gerry Isaac",
address_country: "PH",
billing: {
name: "Gerry Isaac",
phone_number: "639175511222",
email: "client@example.com",
line1: "#123 JP Rizal St.",
line2: "Brgy. Aguinaldo",
city: "Quezon City",
state: "Metro Manila",
country: "PH",
zip_code: "1100"
},
shipping: {
name: "Gerry Isaac",
phone_number: "639175511222",
email: "client@example.com",
line1: "#123 JP Rizal St.",
line2: "Brgy. Aguinaldo",
city: "Quezon City",
state: "Metro Manila",
country: "PH",
zip_code: "1100"
}
},
metadata: {
order_id: "ord_123",
customer_id: "cust_456"
}
});
Parameters
The card number, expiry, and CVC come from the mounted input fields. The name and redirect fields are required. All other fields are optional.
Card fields
| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Cardholder name as it appears on the card |
address_line1 |
string |
No | Card billing address line 1 |
address_line2 |
string |
No | Card billing address line 2 |
address_city |
string |
No | Card billing city |
address_state |
string |
No | Card billing state or province |
address_country |
string |
No | Card billing country (ISO 3166-1 alpha-2, e.g. "PH") |
address_zip |
string |
No | Card billing postal code |
Redirect URLs
The redirect object is required but should be hardcoded in your integration — do not collect these from customers. These URLs handle 3DS authentication flow and payment notifications.
| Field | Type | Required | Description |
|---|---|---|---|
redirect.success |
string |
Yes | URL to redirect to after successful 3DS authentication |
redirect.fail |
string |
Yes | URL to redirect to after failed 3DS authentication |
redirect.notify |
string |
No | Webhook URL for async payment status notifications |
Owner
The owner object carries full billing and shipping contact details. These are separate from the card address fields above and are passed directly to the Magpie API.
| Field | Type | Description |
|---|---|---|
owner.name |
string |
No |
owner.address_country |
string |
No |
owner.billing.name |
string |
Yes |
owner.billing.phone_number |
string |
No |
owner.billing.email |
string |
No |
owner.billing.line1 |
string |
No |
owner.billing.line2 |
string |
No |
owner.billing.city |
string |
No |
owner.billing.state |
string |
No |
owner.billing.country |
string |
No |
owner.billing.zip_code |
string |
No |
owner.shipping.name |
string |
Yes |
owner.shipping.phone_number |
string |
No |
owner.shipping.email |
string |
No |
owner.shipping.line1 |
string |
No |
owner.shipping.line2 |
string |
No |
owner.shipping.city |
string |
No |
owner.shipping.state |
string |
No |
owner.shipping.country |
string |
No |
owner.shipping.zip_code |
string |
No |
Metadata
| Field | Type | Description |
|---|---|---|
metadata |
object or array |
Any custom key-value data you want to attach to the source. Returned as-is in the response. |
Response
On success, the promise resolves with a source object:
{
"object": "source",
"id": "src_019cb34573611648c15c18cd55bd9a4b",
"type": "card",
"card": {
"object": "card",
"id": "card_019cb3457361c1e3716ac60d57556ab0",
"name": "Gerry Isaac",
"last4": "1112",
"exp_month": "12",
"exp_year": "2026",
"address_line1": "#123 JP Rizal St.",
"address_line2": "Brgy. Aguinaldo",
"address_city": "Quezon City",
"address_state": "Metro Manila",
"address_country": "PH",
"address_zip": "1100",
"brand": "visa",
"country": "PH",
"cvc_checked": "",
"funding": "debit",
"issuing_bank": "VISA PRODUCTION SUPPORT CLIENT BID 1"
},
"bank_account": null,
"redirect": {
"success": "https://your-site.com/checkout/success",
"fail": "https://your-site.com/checkout/fail",
"notify": "https://your-site.com/checkout/notify"
},
"owner": {
"billing": {
"name": "Gerry Isaac",
"phone_number": "639175511222",
"email": "client@example.com",
"line1": "#123 JP Rizal St.",
"line2": "Brgy. Aguinaldo",
"city": "Quezon City",
"state": "Metro Manila",
"country": "PH",
"zip_code": "1100"
},
"shipping": {
"name": "Gerry Isaac",
"phone_number": "639175511222",
"email": "client@example.com",
"line1": "#123 JP Rizal St.",
"line2": "Brgy. Aguinaldo",
"city": "Quezon City",
"state": "Metro Manila",
"country": "PH",
"zip_code": "1100"
},
"name": "Gerry Isaac",
"address_country": "PH"
},
"vaulted": false,
"used": false,
"livemode": true,
"created_at": "2026-03-03T18:36:39.304616+08:00",
"updated_at": "2026-03-03T18:36:39.462930+08:00",
"metadata": {}
}
Errors
On failure, the promise rejects with an error object. Always wrap createSource() in a try/catch:
try {
const source = await cardNumber.createSource({ ... });
} catch (err) {
// err.message contains the error description
console.error(err.message);
}
Common error messages:
| Message | Cause |
|---|---|
"Card number is required" |
Card number field is empty |
"Card expiry is required" |
Expiry field is empty |
"CVC is required" |
CVC field is empty |
"Invalid expiry date format" |
Expiry could not be parsed |
"The API key doesn't have permissions to perform the request." |
Wrong key type or invalid key |
"Validation failed: ..." |
Card data failed server-side validation |
9. Event Handling
The SDK fires events as the user interacts with card fields. Listen for them on an element instance.
Note: Event listening is built into the SDK's internal
postMessagehandler. The events listed below are theactionvalues received from the iframes.
FIELD_COMPLETE
Fired when a field has a valid, complete value.
Triggered by:
cardNumber: Luhn-valid number of at least 16 digitscardExpiry: ValidMM / YYwith a future datecardCvc: 3–4 digits entered
The SDK uses this internally to auto-advance focus. You can also use it to drive UI feedback (e.g. green border on complete fields) by listening to postMessage events from the iframe:
window.addEventListener("message", (event) => {
if (event.origin !== "https://components.magpie.im") return;
if (event.data?.action === "FIELD_COMPLETE") {
const field = event.data.field; // "cardNumber" | "cardExpiry" | "cardCvc"
document.querySelector(`#${field}-container`).classList.add("complete");
}
if (event.data?.action === "FIELD_EMPTY") {
const field = event.data.field;
document.querySelector(`#${field}-container`).classList.remove("complete");
}
});
FIELD_EMPTY
Fired when a previously complete field is cleared (e.g. the user backspaces to empty).
READY
Fired by each iframe once it has loaded and validated the INIT message from the SDK. The SDK handles this internally — you do not need to wait for it manually.
SOURCE_CREATED
Fired by the iframe when the Magpie API returns a successful source. Handled internally by the SDK — your createSource() promise resolves with the payload.
SOURCE_ERROR
Fired by the iframe when source creation fails. Handled internally by the SDK — your createSource() promise rejects with the error.
10. postMessage Protocol
This section is for advanced use cases where you are building a custom integration or debugging the SDK internals.
Messages sent by the SDK to each iframe
| Action | Payload | Description |
|---|---|---|
INIT |
{ apiKey, parentOrigin } |
Sent once after the iframe loads. Passes the merchant's public key and the current page origin. |
CREATE_SOURCE |
{ additionalData } |
Triggers source creation. additionalData contains cardholder name, address, redirect URL. |
UPDATE_STYLE |
{ fontFamily, fontSize, color, ... } |
Applies style overrides to the input field inside the iframe. |
CMD_FOCUS |
— | Requests the iframe to focus its input. Used for auto-advance between fields. |
Messages sent by iframes to the SDK
| Action | Payload | Description |
|---|---|---|
READY |
— | Iframe has initialized and is ready. |
FIELD_COMPLETE |
{ field: "cardNumber" \| "cardExpiry" \| "cardCvc" } |
Field value is valid and complete. |
FIELD_EMPTY |
{ field: "cardNumber" \| "cardExpiry" \| "cardCvc" } |
Field was cleared. |
SOURCE_CREATED |
Source object | Source was created successfully. |
SOURCE_ERROR |
{ error, debug } |
Source creation failed. |
Origin validation
- The SDK sends all
postMessagecalls withtargetOriginset tocomponentsUrl(e.g.https://components.magpie.im). Messages are rejected by the browser if the iframe is not at that exact origin. - Each iframe validates that
event.originon received messages matches theparentOriginsent in theINITmessage. - The SDK validates that
event.originon received messages matchescomponentsUrl.
11. API Reference
POST /api/v2/sources
Creates a payment source. This endpoint is called internally by the iframe — you should not call it directly from your page.
Authentication: Authorization: Basic <base64(pk_live_key:)>
Minimal request body (required fields):
{
"type": "card",
"card": {
"name": "Cardholder Name",
"number": "4012001037141112",
"exp_month": "12",
"exp_year": "2025",
"cvc": "123"
},
"redirect": {
"success": "https://your-site.com/success",
"fail": "https://your-site.com/fail"
}
}
Complete request body (all possible fields):
{
"type": "card",
"card": {
"name": "Gerry Isaac",
"number": "4012001037141112",
"exp_month": "12",
"exp_year": "2025",
"cvc": "123",
"address_line1": "#123 JP Rizal St.",
"address_line2": "Brgy. Aguinaldo",
"address_city": "Quezon City",
"address_state": "Metro Manila",
"address_country": "PH",
"address_zip": "1100"
},
"redirect": {
"success": "https://your-site.com/checkout/success",
"fail": "https://your-site.com/checkout/fail",
"notify": "https://your-site.com/checkout/notify"
},
"owner": {
"name": "Gerry Isaac",
"address_country": "PH",
"billing": {
"name": "Gerry Isaac",
"phone_number": "639175511222",
"email": "client@example.com",
"line1": "#123 JP Rizal St.",
"line2": "Brgy. Aguinaldo",
"city": "Quezon City",
"state": "Metro Manila",
"country": "PH",
"zip_code": "1100"
},
"shipping": {
"name": "Gerry Isaac",
"phone_number": "639175511222",
"email": "client@example.com",
"line1": "#123 JP Rizal St.",
"line2": "Brgy. Aguinaldo",
"city": "Quezon City",
"state": "Metro Manila",
"country": "PH",
"zip_code": "1100"
}
},
"metadata": {
"order_id": "ord_123",
"customer_id": "cust_456"
}
}
Response (200):
{
"object": "source",
"id": "src_019cb34573611648c15c18cd55bd9a4b",
"type": "card",
"card": {
"object": "card",
"id": "card_019cb3457361c1e3716ac60d57556ab0",
"name": "Gerry Isaac",
"last4": "1112",
"exp_month": "12",
"exp_year": "2026",
"address_line1": "#123 JP Rizal St.",
"address_line2": "Brgy. Aguinaldo",
"address_city": "Quezon City",
"address_state": "Metro Manila",
"address_country": "PH",
"address_zip": "1100",
"brand": "visa",
"country": "PH",
"cvc_checked": "",
"funding": "debit",
"issuing_bank": "VISA PRODUCTION SUPPORT CLIENT BID 1"
},
"bank_account": null,
"redirect": {
"success": "https://your-site.com/checkout/success",
"fail": "https://your-site.com/checkout/fail",
"notify": "https://your-site.com/checkout/notify"
},
"owner": {
"billing": {
"name": "Gerry Isaac",
"phone_number": "639175511222",
"email": "client@example.com",
"line1": "#123 JP Rizal St.",
"line2": "Brgy. Aguinaldo",
"city": "Quezon City",
"state": "Metro Manila",
"country": "PH",
"zip_code": "1100"
},
"shipping": {
"name": "Gerry Isaac",
"phone_number": "639175511222",
"email": "client@example.com",
"line1": "#123 JP Rizal St.",
"line2": "Brgy. Aguinaldo",
"city": "Quezon City",
"state": "Metro Manila",
"country": "PH",
"zip_code": "1100"
},
"name": "Gerry Isaac",
"address_country": "PH"
},
"vaulted": false,
"used": false,
"livemode": true,
"created_at": "2026-03-03T18:36:39.304616+08:00",
"updated_at": "2026-03-03T18:36:39.462930+08:00",
"metadata": {}
}
Middleware applied:
origin.verify— validates the request'sOriginheader against the allowlistthrottle:300,1— 300 requests per minute per IP
POST /api/v2/charges
Creates a charge against an existing source. Call this from your server using your secret key. Never call it from the browser.
Authentication: Authorization: Basic <base64(sk_live_key:)>
Request body:
{
"amount": 10000,
"currency": "php",
"source": "src_abc123",
"description": "Order #456",
"statement_descriptor": "My Store"
}
| Field | Type | Required | Description |
|---|---|---|---|
amount |
integer |
Yes | Amount in the smallest currency unit (e.g. centavos for PHP) |
currency |
string |
Yes | "php" or "usd" |
source |
string |
Yes | Source ID returned from createSource() |
description |
string |
Yes | Description of the charge |
statement_descriptor |
string |
No | Appears on card statement. Max 22 characters. |
Response (200):
{
"object": "charge",
"id": "ch_xyz789",
"amount": 10000,
"amount_refunded": 0,
"captured": true,
"currency": "php",
"description": "Order #456",
"statement_descriptor": "My Store",
"status": "succeeded",
"source": {
"id": "src_abc123",
"type": "card",
"card": { "last4": "4242", "brand": "visa", ... }
},
"created_at": "2026-03-03T10:00:00Z"
}
GET /api/v2/health
Returns the service status. Use for uptime monitoring.
Response (200):
{ "status": "ok" }
12. Origin Allowlists
Magpie Components enforces two separate allowlists:
| Allowlist | Controls | Environment variable |
|---|---|---|
| Embed allowlist | Which origins may embed the iframe | MAGPIE_EMBED_ALLOWED_ORIGINS |
| API allowlist | Which origins may call /api/v2/sources |
MAGPIE_API_ALLOWED_ORIGINS |
Both must include your origin for the integration to work end-to-end.
Environment-based allowlists (default)
Set comma-separated origins in your .env:
MAGPIE_EMBED_ALLOWED_ORIGINS=https://checkout.your-site.com,https://pay.your-site.com
MAGPIE_API_ALLOWED_ORIGINS=https://components.magpie.im
For regex-based matching (e.g. to cover all subdomains):
MAGPIE_API_ALLOWED_ORIGIN_PATTERNS=/^https:\/\/.*\.your-site\.com$/
Database-based allowlists (per API key)
For fine-grained control where each merchant key has its own allowed origin:
MAGPIE_USE_DB_ALLOWED_ORIGINS=true
Run the migration:
php artisan migrate --force
Add an allowed origin for a specific key:
php artisan magpie:allow-origin pk_live_merchant_key https://checkout.merchant-site.com
When MAGPIE_USE_DB_ALLOWED_ORIGINS=true, the middleware extracts the API key from the Authorization header of each request and checks the allowed_origins table for a matching (api_key, origin) row.
13. Local Development
Step 1 — Set environment variables
In your local .env:
APP_URL=https://elements-dev.your-domain.test
MAGPIE_API_URL=https://api.magpie.im/v2
MAGPIE_EMBED_ALLOWED_ORIGINS=https://your-demo-page.test
MAGPIE_API_ALLOWED_ORIGINS=https://elements-dev.your-domain.test
MAGPIE_USE_DB_ALLOWED_ORIGINS=false
MAGPIE_DEBUG_ROUTES=false
Step 2 — Point the SDK at your local components
In your demo page, pass componentsUrl when initializing:
<script src="https://elements-dev.your-domain.test/sdk/magpie.js"></script>
<script>
const magpie = new Magpie("pk_test_your_key", {
componentsUrl: "https://elements-dev.your-domain.test"
});
</script>
Step 3 — Verify the setup
# Components health check
curl https://elements-dev.your-domain.test/api/v2/health
# Components iframe loads
curl https://elements-dev.your-domain.test/components/index.html
Both should return 200.
Debug routes (optional)
For local debugging only, enable additional diagnostic endpoints:
MAGPIE_DEBUG_ROUTES=true
This enables:
GET /api/v2/debug— tests connectivity to the upstream Magpie APIPOST /api/v2/echo— echoes back headers and body for request inspection
Never enable debug routes in production.
14. Production Setup
Checklist
- [ ]
APP_URLset to your production components domain (e.g.https://components.magpie.im) - [ ]
MAGPIE_API_URLset tohttps://api.magpie.im/v2 - [ ]
MAGPIE_EMBED_ALLOWED_ORIGINSlists all origins that will embed the iframe - [ ]
MAGPIE_API_ALLOWED_ORIGINSincludeshttps://components.magpie.im(the iframe's own origin) - [ ]
MAGPIE_DEBUG_ROUTES=false - [ ] HTTPS enforced on both the components server and all merchant pages
- [ ] Rate limiting configured (default: 300 requests/minute/IP on
/api/v2/sources) - [ ] Secret keys (
sk_live_...) are only used server-side, never in frontend code
Deployment
After any environment variable change, redeploy or restart your application so the new config takes effect. On Laravel Cloud or Forge, trigger a new deployment after updating env vars.
15. Security Model
Card data isolation
Card data is entered directly inside cross-origin iframes served from https://components.magpie.im. Your page's JavaScript cannot read the contents of these iframes due to the browser's same-origin policy. Card data never exists in your DOM or your JavaScript.
Key flow
- You initialize
new Magpie("pk_live_...")with your public key. - The SDK sends the public key to the iframe via
postMessageduringINIT. - The iframe uses the key to authenticate against
/api/v2/sourcesusing HTTP Basic Auth:Authorization: Basic <base64(pk_live_key:)>. - The proxy forwards the key — unchanged — to the upstream Magpie API.
- Your secret key (
sk_live_...) is never involved in source creation.
Origin validation
- Iframe embedding: Controlled by CSP
frame-ancestorsheader, set per-response by theAllowIframeEmbeddingmiddleware. Merchants not on the allowlist cannot embed the iframe. - API calls: The
VerifyRequestOriginmiddleware checks theOrigin(orReferer) header on every call to/api/v2/sources. Origins not on the allowlist receive a403. - postMessage: Both the SDK and the iframe validate
event.originbefore processing any message.
Card data in logs
The backend uses CardDataMasker on all log entries. Card numbers are logged as 483442******4534. CVCs are logged as ***. Authorization headers are logged as [REDACTED]. No plaintext card data or API keys appear in logs.
HTTPS
All communication between the browser, the components server, and the Magpie API must be over HTTPS. HTTP is not supported in production.
16. Error Handling & Troubleshooting
Common errors
403 Origin not allowed
Your page's origin is not on the allowlist.
- Add your origin to
MAGPIE_EMBED_ALLOWED_ORIGINS(for iframe embedding) andMAGPIE_API_ALLOWED_ORIGINS(for API calls). - If using
MAGPIE_USE_DB_ALLOWED_ORIGINS=true, runphp artisan magpie:allow-origin <key> <origin>. - Redeploy after changing env vars.
401 Missing Authorization header
The apiKey was not passed to new Magpie(...), or the INIT message was not received by the iframe before createSource() was called.
- Ensure you pass your public key to
new Magpie("pk_live_..."). - Ensure
mount()is called beforecreateSource().
"The API key doesn't have permissions to perform the request."
The key was received but rejected by the upstream Magpie API.
- Verify you are using a public key (
pk_live_...orpk_test_...), not a secret key. - Verify the key is active and associated with the correct Magpie account.
422 Validation failed
The request is missing required fields or contains invalid data. Common causes:
- Missing
namefield increateSource()call - Missing or incomplete
redirectobject (success, fail, notify URLs are all required) - Card fields (number, expiry, CVC) not filled in
Check the response body for the specific field that failed validation.
502 Connection error
The proxy could not reach the upstream Magpie API (api.magpie.im).
- Check
MAGPIE_API_URLis set correctly. - Verify outbound connectivity from your server to
api.magpie.im.
Iframe loads but card fields are blank
- Check the browser console for CSP or CORS errors.
- Ensure the iframe
srcURL (/components/index.html) returns200. - Ensure
componentsUrlinnew Magpie(...)matches the server serving the components.
createSource() hangs and never resolves
- Check that
mount()was called for all three elements before callingcreateSource(). - Open the browser devtools Network tab and look for a failed request to
/api/v2/sources. - Check the browser console for
postMessageorigin mismatch errors.
Smoke tests
# 1. Health check
curl https://components.magpie.im/api/v2/health
# → {"status":"ok"}
# 2. Components iframe
curl -I https://components.magpie.im/components/index.html
# → HTTP/2 200
# 3. Source creation (replace key and values)
curl -X POST https://components.magpie.im/api/v2/sources \
-H "Authorization: Basic $(echo -n 'pk_test_your_key:' | base64)" \
-H "Content-Type: application/json" \
-H "Origin: https://components.magpie.im" \
-d '{
"type": "card",
"card": {
"name": "Test User",
"number": "4242424242424242",
"exp_month": 12,
"exp_year": 2028,
"cvc": "123"
},
"redirect": {
"success": "https://your-site.com/success",
"fail": "https://your-site.com/fail",
"notify": "https://your-site.com/notify"
}
}'
# → {"object":"source","id":"src_..."}
17. Full Working Example
A complete, copy-paste-ready integration with styling and error handling.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Checkout</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: #F9FAFB;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.card {
background: #fff;
border-radius: 12px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
padding: 32px;
width: 100%;
max-width: 420px;
}
h2 {
margin: 0 0 24px;
font-size: 20px;
font-weight: 600;
color: #111827;
}
label {
display: block;
margin-bottom: 4px;
font-size: 13px;
font-weight: 500;
color: #374151;
}
.field {
border: 1px solid #D1D5DB;
border-radius: 8px;
padding: 10px 12px;
height: 44px;
margin-bottom: 16px;
transition: border-color 0.15s;
}
.field:focus-within {
border-color: #6366F1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
}
.row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
button {
width: 100%;
padding: 12px;
background: #6366F1;
color: #fff;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
margin-top: 8px;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.error {
color: #DC2626;
font-size: 13px;
margin-top: 12px;
min-height: 20px;
}
.success {
color: #059669;
font-size: 13px;
margin-top: 12px;
}
</style>
</head>
<body>
<div class="card">
<h2>Payment details</h2>
<label>Card number</label>
<div class="field" id="card-number"></div>
<div class="row">
<div>
<label>Expiry</label>
<div class="field" id="card-expiry"></div>
</div>
<div>
<label>CVC</label>
<div class="field" id="card-cvc"></div>
</div>
</div>
<button id="pay-btn">Pay ₱100.00</button>
<div class="error" id="error-msg"></div>
<div class="success" id="success-msg"></div>
</div>
<script src="https://components.magpie.im/sdk/magpie.js"></script>
<script>
const magpie = new Magpie("pk_live_your_key_here");
const elements = magpie.elements();
const cardStyle = {
style: {
base: {
fontFamily: "system-ui, -apple-system, sans-serif",
fontSize: "15px",
color: "#111827",
"::placeholder": { color: "#9CA3AF" }
}
}
};
const cardNumber = elements.create("cardNumber", cardStyle);
const cardExpiry = elements.create("cardExpiry", cardStyle);
const cardCvc = elements.create("cardCvc", cardStyle);
cardNumber.mount("#card-number");
cardExpiry.mount("#card-expiry");
cardCvc.mount("#card-cvc");
const payBtn = document.getElementById("pay-btn");
const errorMsg = document.getElementById("error-msg");
const successMsg = document.getElementById("success-msg");
payBtn.addEventListener("click", async () => {
errorMsg.textContent = "";
successMsg.textContent = "";
payBtn.disabled = true;
payBtn.textContent = "Processing…";
try {
const source = await cardNumber.createSource({
name: "Cardholder Name", // Required field
redirect: {
success: "https://your-site.com/checkout/success",
fail: "https://your-site.com/checkout/fail",
notify: "https://your-site.com/checkout/notify"
}
});
// Send source.id to your server to complete the charge
successMsg.textContent = `Source created: ${source.id}`;
// Example: send to your backend
// await fetch("/your-server/charge", {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify({ source: source.id, amount: 10000 })
// });
} catch (err) {
errorMsg.textContent = err?.message || "An unexpected error occurred.";
} finally {
payBtn.disabled = false;
payBtn.textContent = "Pay ₱100.00";
}
});
</script>
</body>
</html>
For questions or issues, contact the Magpie integrations team or open a support ticket.
↑ Back to top