Myela Payments API
Version v1 — one API, whichever provider settles the money. 44 endpoints across 9 sections.
Overview
Start with GET /v1/capabilities. It returns every operation and whether it is active, unavailable (your key lacks the entitlement) or not_supported (the provider cannot do it). Branch on that at integration time rather than discovering a 403 in production.
Authentication
Authorization: Bearer myela_sk_test_…
Keys are issued by Myela and shown once. They are stored hashed and cannot be recovered; a revoked key stops working immediately.
A test key authenticates with the bearer token alone. A live key must additionally HMAC-sign every request — an unsigned live key is refused with
401 This key requires request signing. That is why curl and Postman are fine for test keys and unsuitable for live ones.| Key | Prefix | Lives | Can |
|---|---|---|---|
| Secret | myela_sk_ | your server | everything, including moving money |
| Publishable | myela_pk_ | the browser | start a checkout session and exchange a token — nothing else, and only from an origin on its allow-list |
Never put a secret key in a browser, a mobile app, or a repository.
Collections
Every route on this page, ready to run. Both are generated from the same route table as the reference below, so they cannot describe an endpoint that no longer exists.
No credentials are included. Each ships a MYELA_API_KEY placeholder — paste a test key from Settings → API. A live key additionally requires request signing, which neither tool does out of the box.
Both default to https://payments-api-sandbox.merchantservicedepot.com. Point BASE_URL at your own host to run them elsewhere.
Money
4999 is $49.99.
Sending
49.99 is rejected rather than coerced — a float that silently becomes 4999 today becomes 4998 the day it arrives from an arithmetic expression, and that is a settlement discrepancy nobody traces back to the API.Card capture
Your page mounts the card field with a publishable key; the shopper types their card; you receive a single-use
paymentToken; your server sends that token to POST /v1/payments to charge it, or POST /v1/payment_methods to store it. A token is single-use and short-lived — mint a fresh one per operation.Myela Elements
Server contract live; hosted frame not yet. The two routes below — POST /v1/checkout_sessions and POST /v1/tokens — are deployed and can be called today, and they are documented in the collections above. What is not serving yet is js.myela.com, so the middle step (mounting the fields and getting a provider token) has no hosted implementation. Until it does, capture a card with the field your Myela contact provides and exchange the result at POST /v1/tokens, or send it straight to POST /v1/payments as a paymentToken.
A session can only start from an origin you have registered. Your publishable key carries an allow-list you manage. It starts EMPTY, and an empty list refuses every origin — a checkout that returns 401 until you add your domain is recoverable; one open to the whole internet is not.
<script type="module">
import { Myela } from 'https://js.myela.com/v1/elements.js';
const myela = Myela({ publishableKey: 'myela_pk_test_…' });
const card = myela.elements().create('card');
await card.mount('#card-field');
card.on('change', (e) => (payButton.disabled = !e.complete));
payButton.addEventListener('click', async () => {
const { token, error } = await card.tokenize();
if (error) return showError(error.message);
// token is opaque: "mtok_…". Send it to YOUR server.
await fetch('/checkout', { method: 'POST', body: JSON.stringify({ token }) });
});
</script>
Then, server-side, the token is just a paymentToken:
await myela.payments.create({ amount: 4999, currency: 'USD', paymentToken: token });
Use the publishable key here (myela_pk_), never the secret one. A publishable key is meant to be readable by anyone who views the page; a secret key in a browser publishes your ability to move money to every visitor, and Elements rejects one outright.
The token is single-use and expires in about two minutes. Mint a fresh one per payment.
Playground
bun install
bun run playground # → http://localhost:4477
One process doing three jobs: the UI, an in-memory mock of this API, and scenarios that drive the real SDK against that mock over real HTTP. So what you observe is the SDK's genuine behaviour rather than a description of it.
Nothing there can move money. No credentials, no network calls beyond loopback, no settlement.
Test tokens select the outcome, because you cannot rehearse a decline by waiting for a real card to be declined:
tok_ok authorizes in full, tok_partial authorizes half the requested amount (status partially_authorized), tok_insufficient declines with a soft error a retry may clear.The mock is a third dialect. Its outcomes are its own — they do not reproduce against a real provider's sandbox, where the card number or the request decides. Use it to learn the SDK's shape, never to predict what a specific card will do.
Pagination
GET /v1/customers?limit=25
GET /v1/customers?limit=25&startingAfter=cus_abc123
{ "object": "list", "data": [ … ], "hasMore": true, "nextCursor": "cus_xyz789" }
limit defaults to 10 and caps at 100. Follow nextCursor by passing it as startingAfter. It is present only when hasMore is true, so a loop that stops on hasMore always terminates. A cursor naming no row is a 422, never a silent restart at page one.Errors
{ "error": { "code": "invalid_request", "message": "…" } }
Branch on code, never on message. Codes are a contract; messages are prose and get improved.
An object belonging to another merchant is
404, never 403 — confirming it exists would leak that it exists. 501 is a permanent refusal: do not retry it, check capabilities instead.| HTTP | code | Means |
|---|---|---|
401 | authentication_failed | missing, malformed, revoked, or unsigned live key |
403 | permission_denied | valid key, but not permitted to perform that operation |
403 | capability_not_enabled | the operation exists but is not enabled for your account — the capability field names it. Ask your Myela contact to enable it; unlike 501 this is not permanent |
404 | not_found | no such object on your account |
409 | conflict | illegal state transition, e.g. voiding a settled payment |
422 | invalid_request | the request is wrong — bad amount, unknown cursor, missing field |
500 | api_error | our fault; safe to retry |
501 | capability_not_supported | permanent refusal — do not retry |
Idempotency
Idempotency-Key: <unique-string> on any money-moving request. A retry with the same key returns the original result instead of charging twice. Use a fresh key per logical operation, not per retry.Start here
Run this first. It answers what this key may do, and it is the only route that is not entitlement-gated — asking what you may do must never itself require permission.
Capabilities
Every operation, and whether it is active, unavailable (this key lacks the entitlement) or not_supported (the account's provider cannot do it). Branch on this rather than discovering a 403 in production.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/capabilities" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/capabilities", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/capabilities",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Payments
The money-moving surface. Amounts are integers in the minor unit — 4999 is $49.99. A decimal is rejected rather than silently charged as a different figure.
A payment is created against exactly ONE funding source: a paymentToken from browser capture, or a stored paymentMethodId. Sending both is refused, because which card was charged is not a question to answer afterwards.
Create payment (sale)
capture: true authorizes and captures in one call. Set an Idempotency-Key header on retries: the same key returns the original result rather than charging twice.
Amounts are integers in the minor unit — 4999 is $49.99. A decimal is rejected rather than silently charged as a different figure.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payments" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amount": 1299,
"currency": "USD",
"paymentToken": "{{PAYMENT_TOKEN}}",
"capture": true
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"amount": 1299,
"currency": "USD",
"paymentToken": "{{PAYMENT_TOKEN}}",
"capture": true
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"amount": 1299,
"currency": "USD",
"paymentToken": "{{PAYMENT_TOKEN}}",
"capture": True
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Create payment (auth only)
Holds funds without taking them. Capture it later, or void it. An uncaptured authorization expires on the provider's schedule, not ours.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payments" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amount": 2500,
"currency": "USD",
"paymentToken": "{{PAYMENT_TOKEN}}",
"capture": false
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"amount": 2500,
"currency": "USD",
"paymentToken": "{{PAYMENT_TOKEN}}",
"capture": false
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"amount": 2500,
"currency": "USD",
"paymentToken": "{{PAYMENT_TOKEN}}",
"capture": False
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Capture a payment
Only valid on an open authorization. Omit amount to capture the full authorized amount. Whether a PARTIAL capture is permitted depends on the provider behind the account — check capabilities rather than assuming.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{auth_id}/capture" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amount": 2500
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{auth_id}/capture", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"amount": 2500
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{auth_id}/capture",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"amount": 2500
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Void a payment
Cancels an unsettled transaction. Once settled, a void is refused and a refund is the correct operation — the state machine enforces this rather than letting the provider decide.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}/void" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}/void", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}/void",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Refund a payment
Returns money on a captured transaction. Omit amount for a full refund. Partial refunds may be repeated up to the captured total.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}/refund" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amount": 500
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}/refund", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"amount": 500
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}/refund",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"amount": 500
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve a payment
The authoritative state. status reflects settlement as reported by the provider, not an optimistic local guess.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List payments
Cursor-paginated — see the collection description. startingAfter takes the id of the last row you saw.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/payments?limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments?limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments?limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Customers
A customer is the thing cards and subscriptions attach to. Creating one reserves it with the provider as well, so the same customer works for one-off charges and for recurring billing.
Create customer
Only email is required. name falls back to the local part of the email when omitted.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/customers" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "{{$guid}}@example.com",
"name": "Ada Lovelace",
"phone": "+15555550123"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/customers", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"email": "{{$guid}}@example.com",
"name": "Ada Lovelace",
"phone": "+15555550123"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/customers",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"email": "{{$guid}}@example.com",
"name": "Ada Lovelace",
"phone": "+15555550123"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve customer
A customer id from another merchant is not_found, never permission_denied — confirming existence would leak it.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/customers/{customer_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/customers/{customer_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/customers/{customer_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Update customer
POST, not PATCH — the update convention across this API. Only the fields you send change.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/customers/{customer_id}" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Ada King"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/customers/{customer_id}", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Ada King"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/customers/{customer_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"name": "Ada King"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List customers
Cursor-paginated.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/customers?limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/customers?limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/customers?limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Payment methods
Cards on file. The card itself is captured in the browser and never reaches your server — you exchange a single-use token for a reusable payment method here.
Create payment method (vault a card)
Consumes the single-use token and returns a reusable id. A card stored through /v1 with no stated schedule is credential-on-file unscheduled — the merchant charges it when they charge it.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customerId": "{{CUSTOMER_ID}}",
"paymentToken": "{{PAYMENT_TOKEN}}"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"customerId": "{{CUSTOMER_ID}}",
"paymentToken": "{{PAYMENT_TOKEN}}"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"customerId": "{{CUSTOMER_ID}}",
"paymentToken": "{{PAYMENT_TOKEN}}"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List payment methods
customerId is required — cards are always listed for one customer. The default card sorts first.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods?customerId={{CUSTOMER_ID}}&limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods?customerId={{CUSTOMER_ID}}&limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods?customerId={{CUSTOMER_ID}}&limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Set default payment method
Which card is charged when a request names none — including a scheduled invoice collecting itself.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods/{payment_method_id}/default" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods/{payment_method_id}/default", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods/{payment_method_id}/default",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Addresses
Billing and shipping addresses, held by Myela and mirrored to the provider where it can express them — so an address survives a change of provider.
Create address
Fields are allow-listed: anything else you send is not stored.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/addresses" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customerId": "{{CUSTOMER_ID}}",
"firstName": "Ada",
"lastName": "Lovelace",
"line1": "12 Marylebone Road",
"city": "London",
"postalCode": "NW1 5JD",
"country": "GB"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/addresses", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"customerId": "{{CUSTOMER_ID}}",
"firstName": "Ada",
"lastName": "Lovelace",
"line1": "12 Marylebone Road",
"city": "London",
"postalCode": "NW1 5JD",
"country": "GB"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/addresses",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"customerId": "{{CUSTOMER_ID}}",
"firstName": "Ada",
"lastName": "Lovelace",
"line1": "12 Marylebone Road",
"city": "London",
"postalCode": "NW1 5JD",
"country": "GB"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List addresses
Scoped to one customer.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/addresses?customerId={{CUSTOMER_ID}}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/addresses?customerId={{CUSTOMER_ID}}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/addresses?customerId={{CUSTOMER_ID}}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve address
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Update address
Only the fields you send change.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"city": "Manchester"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"city": "Manchester"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"city": "Manchester"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Set default address
type is required and is either billing or shipping — a customer has one default of each, so a request that does not say which is refused rather than guessed.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}/default" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "billing"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}/default", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"type": "billing"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}/default",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"type": "billing"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Plans and subscriptions
A plan is the price and cadence; a subscription binds a customer, a plan and a card. Amounts are integers in the minor unit — 4999 is $49.99. A decimal is rejected rather than silently charged as a different figure.
Create plan
interval is one of day, week, month, year. An unknown interval is refused here rather than upstream.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/plans" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Standard monthly",
"amount": 4999,
"currency": "USD",
"interval": "month",
"intervalCount": 1
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/plans", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"name": "Standard monthly",
"amount": 4999,
"currency": "USD",
"interval": "month",
"intervalCount": 1
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/plans",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"name": "Standard monthly",
"amount": 4999,
"currency": "USD",
"interval": "month",
"intervalCount": 1
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve plan
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/plans/{plan_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/plans/{plan_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/plans/{plan_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List plans
Cursor-paginated.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/plans?limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/plans?limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/plans?limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Create subscription
Which calendar day a charge lands on is a timezone question, so timezone is explicit and defaults to UTC rather than being guessed.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customerId": "{{CUSTOMER_ID}}",
"planId": "{{PLAN_ID}}",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}",
"startAt": "2026-09-01",
"timezone": "America/New_York"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"customerId": "{{CUSTOMER_ID}}",
"planId": "{{PLAN_ID}}",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}",
"startAt": "2026-09-01",
"timezone": "America/New_York"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"customerId": "{{CUSTOMER_ID}}",
"planId": "{{PLAN_ID}}",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}",
"startAt": "2026-09-01",
"timezone": "America/New_York"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve subscription
nextBillingAt is absent once cancelled — a stale date there is the shape of every "we cancelled but it billed again".
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions/{subscription_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions/{subscription_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions/{subscription_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List subscriptions
Cursor-paginated.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions?limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions?limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions?limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Cancel subscription
Cancels immediately.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions/{subscription_id}/cancel" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions/{subscription_id}/cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions/{subscription_id}/cancel",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Invoices and scheduled collection
An invoice can be sent for someone to pay, or scheduled to collect itself from a saved card on a date. The schedule is Myela's own: no provider behind this API has a collect-on-a-date primitive.
Create invoice
The invoice total, in minor units — there is no line-item array on this endpoint. Amounts are integers in the minor unit — 4999 is $49.99. A decimal is rejected rather than silently charged as a different figure.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"customerId": "{{CUSTOMER_ID}}",
"currency": "USD",
"amount": 150000,
"description": "Consulting, August",
"dueAt": "2026-09-01T00:00:00.000Z"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"customerId": "{{CUSTOMER_ID}}",
"currency": "USD",
"amount": 150000,
"description": "Consulting, August",
"dueAt": "2026-09-01T00:00:00.000Z"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"customerId": "{{CUSTOMER_ID}}",
"currency": "USD",
"amount": 150000,
"description": "Consulting, August",
"dueAt": "2026-09-01T00:00:00.000Z"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve invoice
url is a Myela-hosted payment page on our own domain, safe to send to a cardholder.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List invoices
Cursor-paginated.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices?limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices?limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices?limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Schedule invoice collection
Charges the named card on that date. Omit paymentMethodId to use the customer's default. Failed attempts retry on a backoff and are visible under Attempts.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/schedule" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"collectAt": "2026-09-01T09:00:00.000Z",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/schedule", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"collectAt": "2026-09-01T09:00:00.000Z",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/schedule",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"collectAt": "2026-09-01T09:00:00.000Z",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Cancel scheduled collection
Leaves the invoice open but stops it collecting itself.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/cancel_schedule" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/cancel_schedule", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/cancel_schedule",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List collection attempts
Every attempt made to collect it, in order — the answer to "why has this not been paid".
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/attempts" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/attempts", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/attempts",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Void invoice
Terminal. A voided invoice cannot be collected or reopened.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/void" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/void", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/void",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Webhook endpoints and deliveries
Where your payment events leave Myela. Entitlements here are separate from the rest of /v1 on purpose: a key that may read invoices should not be able to point that stream somewhere new.
Verify every delivery signature before acting on it — see docs/gateway/api/WEBHOOK_SIGNATURE.md.
Create webhook endpoint
The signing secret is returned once, at creation, and never again.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/myela",
"events": [
"invoice.paid",
"payment.settled",
"payment.refunded"
]
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://example.com/webhooks/myela",
"events": [
"invoice.paid",
"payment.settled",
"payment.refunded"
]
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"url": "https://example.com/webhooks/myela",
"events": [
"invoice.paid",
"payment.settled",
"payment.refunded"
]
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List webhook endpoints
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve webhook endpoint
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Update webhook endpoint
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [
"invoice.paid"
]
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"events": [
"invoice.paid"
]
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"events": [
"invoice.paid"
]
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Delete webhook endpoint
Request
curl -X DELETE "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.delete(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List deliveries
Every attempt to reach your endpoint, with the response we got.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries?limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries?limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries?limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve delivery
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries/{webhook_delivery_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries/{webhook_delivery_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries/{webhook_delivery_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Replay delivery
Re-sends the FROZEN payload, not a rebuilt one — a replay must deliver what the event said when it happened.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries/{webhook_delivery_id}/replay" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries/{webhook_delivery_id}/replay", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries/{webhook_delivery_id}/replay",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Myela Elements (browser)
The only two routes a merchant's own checkout page calls, and the only two authenticated by the PUBLISHABLE key (myela_pk_) rather than the secret one. Set MYELA_PUBLISHABLE_KEY in the environment; the secret key must never reach a browser.
A session can only be started from an origin on that key's allow-list, which the merchant manages. An empty list refuses every origin — "not configured yet" fails closed, because a checkout that 401s until someone adds a domain is recoverable and one open to the internet is not.
The hosted frame at js.myela.com is not serving yet, so the middle step — mounting the fields and getting an upstream token — cannot be done from these two requests alone. The server contract below is live and can be exercised today.
Create checkout session
Opens a checkout session from the merchant's page. intent is one_time (charge now) or store (vault the card); the whole body is optional and defaults to one_time.
Returns sessionId, a mountUrl to iframe, parentOrigin, and an opaque capture object. Nothing in the response names a provider or differs by which one settles the account — that uniformity is the contract, not an implementation detail.
401 means the calling origin is not on this key's allow-list.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/checkout_sessions" \
-H "Authorization: Bearer $MYELA_PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"intent": "one_time"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/checkout_sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_PUBLISHABLE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"intent": "one_time"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/checkout_sessions",
headers={
"Authorization": f"Bearer {os.environ['MYELA_PUBLISHABLE_KEY']}",
},
json={
"intent": "one_time"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Exchange for a payment token
Exchanges the provider token the fields produced for an opaque mtok_, which is what POST /v1/payments accepts as paymentToken.
The session id is the capability — there is no key on this call beyond the publishable one. A session exchanges ONCE; a second attempt is refused, and so is a replay of the resulting mtok_.
Both variables come from the frame, so this request cannot be driven from the collection alone until js.myela.com is serving.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/tokens" \
-H "Authorization: Bearer $MYELA_PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "{{CHECKOUT_SESSION_ID}}",
"upstreamToken": "{{UPSTREAM_TOKEN}}"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/tokens", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_PUBLISHABLE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"sessionId": "{{CHECKOUT_SESSION_ID}}",
"upstreamToken": "{{UPSTREAM_TOKEN}}"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/tokens",
headers={
"Authorization": f"Bearer {os.environ['MYELA_PUBLISHABLE_KEY']}",
},
json={
"sessionId": "{{CHECKOUT_SESSION_ID}}",
"upstreamToken": "{{UPSTREAM_TOKEN}}"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()