About create-merchant-platform
create-merchant-platform guides developers through submitting new merchant onboarding data to the Payroc API. It covers authentication, request fields, idempotency keys, and response and error handling.
Key features
- 01Obtain a bearer token
- 02Build merchant and account requests
- 03Submit with an idempotency key
- 04Store returned identifiers
- 05Handle validation and API errors
Use cases
- 01Submit a new merchant application
- 02Set up merchant processing accounts
- 03Troubleshoot request validation
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ payroc/ skills --skill create-merchant-platformAfter installing, type/create-merchant-platformin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/_sources.mdapi-schema.mderror-response-format.mdidentity-call.mdDetails
From the project READMECreate Merchant Platform
Version check (run this first)
Before announcing anything or starting the flow, confirm this skill is current:
- Read this skill's version from the
metadata.versionfield in the frontmatter above. - Fetch the published copy and read its
metadata.version:https://raw.githubusercontent.com/payroc/skills/main/plugins/payroc/boarding/skills/create-merchant-platform/SKILL.md - Compare the two as semantic versions:
-
This version >= published → continue silently, no message. (A developer running an unreleased newer version is expected and fine.)
-
This version < published → tell the developer:
⚠️ A newer version of this skill (v<published>) has been published — you're running v<current>. Upgrading is recommended for the best results.
Then ask whether they'd like to continue with the current version or stop and upgrade first, and honour their answer.
-
Couldn't fetch (offline, network error, 404) → note briefly that the version couldn't be verified and continue.
-
POST https://api.payroc.com/v1/merchant-platforms initiates the boarding process for a new
merchant. Payroc reviews the submission and assigns a merchantPlatformId and
processingAccountId — save both, as they're required for terminal ordering and all follow-on
boarding operations.
For the complete field reference and an annotated example request, read
references/api-schema.md (load it when you need enum values, nested field details, or a
full worked example).
Quick reference
POST https://api.payroc.com/v1/merchant-platforms
Authorization: Bearer <token>
Idempotency-Key: <uuid-v4>
Content-Type: application/json
Step 1 — Get a bearer token
Read
references/identity-call.mdbefore emitting any auth code. Do not guess the endpoint URL, header name, or response shape — use only what the reference documents.
Tokens expire in ~1 hour. Exchange your API key before each session (or refresh proactively).
# Test / sandbox
curl -X POST https://identity.uat.payroc.com/authorize \
-H "x-api-key: YOUR_API_KEY"
# Production
curl -X POST https://identity.payroc.com/authorize \
-H "x-api-key: YOUR_API_KEY"
Response:
{
"access_token": "eyJhbGc....",
"expires_in": 3600,
"token_type": "Bearer"
}
Use Authorization: Bearer <access_token> on every subsequent request.
Step 2 — Build the request body
The body has two required top-level keys:
{
"business": { ... },
"processingAccounts": [ { ... } ],
"metadata": {}
}
metadata is optional — use it to attach your own key/value pairs (e.g. your internal
customer ID) that Payroc echoes back in responses.
Business object
Describes the legal entity. Required fields:
| Field | Type | Notes |
|---|---|---|
name | string | Legal business name |
taxId | string | EIN (US) |
organizationType | enum | See schema reference for values |
countryOfOperation | string | "US" |
addresses | array | Must include one entry with "type": "legalAddress" |
contactMethods | array | Must include at least one "type": "email" entry |
Processing accounts
Each element provisions one MID. The array must have at least one entry. Key required fields per account:
| Field | Notes |
|---|---|
doingBusinessAs | Trading/DBA name |
merchandiseOrServiceSold | Plain-English description |
businessStartDate | YYYY-MM-DD |
timezone | e.g. "America/New_York" — see schema reference for full list |
address | Physical business address |
contactMethods | Email required |
owners | See ownership rules below |
processing | Transaction volumes and card acceptance |
funding | Bank account and settlement schedule |
pricing | Pricing intent ID or full pricing agreement |
signature | How the merchant signs the contract |
addendums | Array — [] if no special forms apply, see Addendums below |
businessType (retail, restaurant, internet, moto, lodging, notForProfit) and
categoryCode (4-digit MCC integer) are optional — send them when known, since downstream
boarding review still uses them. processor (tsys | fiserv, default tsys) is also
optional; send it explicitly rather than relying on the default.
Addendums — ask whether the merchant needs any of these forms sent with the agreement:
installment payments, money services, telehealth, firearms, pharmacy CNP compliance, CBD,
tobacco CNP, donations, Clover equipment, or Roc Giving. addendums is required on the
request but [] is a valid answer if none apply. See references/api-schema.md#addendums
for the per-type payload shapes (Clover and Roc Giving carry real fields; the rest are
attestation-only).
Ownership rules — every processing account needs:
- Exactly one owner with
relationship.isControlProng: true(only one control prong is allowed) - At least one owner with
relationship.isAuthorizedSignatory: true
A single owner cannot be both — the API rejects an owner with both flags set ("it must be
one or the other or neither"), so you need at least two owners (one control prong, a different
authorized signatory). Each owner also needs firstName, lastName, dateOfBirth
(YYYY-MM-DD), address, identifiers (national ID / SSN), and an email in
contactMethods.
Pricing shortcut — if you have a pricing template, use:
{ "type": "intent", "pricingIntentId": "<id>" }
Otherwise provide the full agreement object (see schema reference for structure).
Signature — send a signing link by email with:
{ "type": "requestedViaEmail" }
Or use "requestedViaDirectLink" with a HATEOAS link object if you're managing the redirect.
Volume breakdown — processing.volumeBreakdown.cardPresent + mailOrTelephone + ecommerce
must sum to exactly 100.
Amounts are in cents — transactionAmounts.average: 5000 means $50.00.
Step 3 — Send the request
Always generate a fresh UUID v4 for Idempotency-Key. The key is bound to the request body, so
reuse the same key only to retry a byte-for-byte identical submission — the API then returns the
original response instead of creating a duplicate. On a genuinely new submission, or when you
resubmit after fixing a 400 (the corrected body no longer matches the old key), generate a new
UUID — reusing the old key with a changed body returns 409.
curl -X POST https://api.payroc.com/v1/merchant-platforms \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
-H "Content-Type: application/json" \
-d @merchant-payload.json
Step 4 — Handle the response
201 Created — boarding submission accepted:
{
"merchantPlatformId": "MP-XXXX",
"processingAccounts": [
{
"processingAccountId": "PA-XXXX",
"status": "entered" // may also be "pending" depending on timing
}
],
"links": [ ... ]
}
Persist merchantPlatformId and each processingAccountId immediately. The initial status
is typically "entered" (received, not yet reviewed) but may arrive as "pending" depending
on processing timing — poll or use webhooks to detect status changes rather than relying on
the create response value.
IDs are opaque. The MP-XXXX / PA-XXXX / PI-XXXX forms in these examples are for
readability only. Treat every ID as an opaque string whose format is not guaranteed and varies by
environment — in UAT they come back as plain integers (e.g. 9815). Don't validate or parse them
against a PREFIX-XXXX pattern.
Step 5 — Handle errors
Errors follow RFC 7807 and include a type
URL linking to Payroc docs, plus an errors array for validation failures.
| Status | Scenario | Action |
|---|---|---|
| 400 validation | Field issues | Fix each field in errors[].parameter; resubmit with a fresh idempotency key (the corrected body needs a new key) |
400 idempotencyKeyMissing | Missing header | Add Idempotency-Key: <uuid-v4> to the request |
| 400 addendum/processor validation | Unrecognized or duplicate addendums[].type, an addendum missing a required field, or an unrecognized processor | Not a fatal or unexpected error — see the addendum/processor error table in references/api-schema.md; fix the flagged field and resubmit |
| 401 | Token expired or invalid | Re-authenticate and get a fresh bearer token |
| 403 | Insufficient permissions | Check API key scope; contact Payroc support |
409 resourceAlreadyExists | Duplicate merchant | Check if merchant was already boarded; retrieve existing record |
409 idempotencyKeyInUse | Key reused with different payload | Generate a new UUID for the new submission |
409 taxIdInUse | Merchant with that Tax ID exists | Retrieve existing merchant platform instead |
| 500 | Server error | Retry with exponential backoff; surface errors array if present |
Reading validation errors — the response uses the RFC 7807 problem-details envelope (type,
title, status, detail, instance); Payroc extends it with an errors array that RFC 7807
does not define. Each errors[] item has a parameter (the JSON path of the failing field), a
detail (short reason — distinct from the top-level detail), and a message (human-readable
explanation). See references/error-response-format.md for the envelope shape and the canonical
error type catalog. A real 400 from UAT (an empty body):
{
"type": "https://docs.payroc.com/api/errors#bad-request",
"title": "Bad request",
"status": 400,
"detail": "One or more validation errors occurred, see error section for more info",
"instance": "https://api.uat.payroc.com/v1/merchant-platforms",
"errors": [
{
"parameter": "merchantPlatform.business",
"detail": "Invalid value",
"message": "'business' must not be empty."
},
{
"parameter": "merchantPlatform.processingAccounts",
"detail": "Required field not populated",
"message": "'processingAccounts' must not be empty."
}
]
}
Each parameter path maps to the request body field that failed — use it to pinpoint and fix each
issue. Fix all flagged fields in a single corrected payload and resubmit. (The published OpenAPI
spec lists only message on each item, but the live API also returns parameter and detail —
verified against UAT, 2026-06-16.)
Common pitfalls
- No control prong / authorized signatory: Both roles are required in every processing account's
ownersarray — but one owner cannot hold both (the API rejects it), so use at least two owners (one control prong, a different authorized signatory) - Phone/fax
valuewith non-digits:phone/mobile/faxvaluemust contain digits only — no+, spaces, or punctuation (e.g.5125550199, not+1 512 555 0199) - Volume breakdown doesn't sum to 100:
cardPresent + mailOrTelephone + ecommercemust equal exactly 100; ifecommerce > 0, the account'swebsiteis required - Amounts in cents: All monetary values (
transactionAmounts,monthlyAmounts,acceleratedFundingFee) are integers in cents - Date format:
dateOfBirthandbusinessStartDatemust beYYYY-MM-DD - Missing idempotency key: All POST requests require
Idempotency-Keyor you get a 400 - Reusing a key for a different payload: Each unique submission needs its own UUID
Full field reference
Read references/api-schema.md for:
- All enum values (organizationType, businessType, timezone, fundingSchedule, etc.)
- Complete nested object schemas (owners, processing, funding, pricing, signature)
- An annotated end-to-end example request body