OutlinePay Docs문서 openapi.json
Quickstart시작하기

Take your first payment첫 결제 받아 보기

This guide walks the whole flow end to end: create a merchant, attach a processor, put the merchant through onboarding, charge a customer on a hosted checkout page, and receive a signed webhook when it settles. Every call uses your test key, so nothing here moves real money.이 가이드는 전체 흐름을 처음부터 끝까지 따라갑니다. 가맹점을 생성하고, 결제 처리사를 연결하고, 가맹점 온보딩을 진행한 뒤, 호스팅 체크아웃 페이지에서 고객에게 결제를 받고, 결제가 완료되면 서명된 웹훅을 수신합니다. 모든 호출에 테스트 키를 사용하므로 실제 자금은 이동하지 않습니다.

  • Create a merchant: the seller you take money for.가맹점 생성: 귀사가 대신 결제를 받아 주는 판매자입니다.
  • Attach a processor: opens its account. Restricted at first, so it still can't be charged.결제 처리사 연결: 가맹점의 계정을 개설합니다. 처음에는 제한된 상태이므로 아직 결제를 받을 수 없습니다.
  • Onboard the merchant: render an embedded component on your page so the merchant finishes verification. This step can't be skipped.가맹점 온보딩: 귀사의 페이지에 임베드 컴포넌트를 렌더링해 가맹점이 검증을 완료하도록 합니다. 이 단계는 건너뛸 수 없습니다.
  • Take a payment: refused with 409 merchant_not_onboarded until the step above finishes.결제 받기: 위 단계가 끝나기 전에는 409 merchant_not_onboarded로 거부됩니다.
  • Get the result: by signed webhook, never by redirect.결과 확인: 리디렉션이 아니라 서명된 웹훅으로 확인합니다.

Prefer the full endpoint list? See the API Reference. Amounts are always integers in the currency's minor unit: 10000 is $100.00 in USD.전체 엔드포인트 목록을 보시려면 API 레퍼런스를 참고해 주십시오. 금액은 항상 해당 통화의 최소 단위 정수이며, USD에서 10000은 $100.00입니다.

Your API keysAPI 키

Authenticate every request with your partner secret key as a Bearer token. Keys come in two variants over the same API: test (olp_sk_test_…) runs on the processor's sandbox and moves no real money; live (olp_sk_live_…) is the real thing. A key is shown once at creation and stored hashed, so save it somewhere safe.모든 요청은 파트너 시크릿 키를 Bearer 토큰으로 사용해 인증합니다. 키는 동일한 API에서 두 종류로 발급됩니다. 테스트 키(olp_sk_test_…)는 결제 처리사의 샌드박스에서 동작하며 실제 자금을 이동시키지 않고, 라이브 키(olp_sk_live_…)는 실제 거래에 사용됩니다. 키는 생성 시 한 번만 표시되고 해시된 형태로 저장되므로 안전한 곳에 보관해 주십시오.

Here is a first authenticated request, listing merchants. Swap in your own test key and run it.아래는 가맹점 목록을 조회하는 첫 인증 요청입니다. 발급받으신 테스트 키로 바꾸어 실행해 보십시오.

<?php
$ch = curl_init("https://api.outlinepay.com/v1/merchants?limit=20");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer olp_sk_test_YOUR_KEY",
    ],
]);

$response = curl_exec($ch);
$status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status . PHP_EOL;
echo $response . PHP_EOL;

Create a merchant가맹점 생성

A merchant is the seller you charge on behalf of. Create one, and keep the returned id (mrc_…). You pass it as merchant_id on every payment.가맹점은 귀사가 대신 결제를 받아 주는 판매자입니다. 가맹점을 생성하시고 반환된 ID(mrc_…)를 보관해 주십시오. 이후 모든 결제에서 merchant_id로 전달하시게 됩니다.

<?php
$ch = curl_init("https://api.outlinepay.com/v1/merchants");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer olp_sk_test_YOUR_KEY",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        "name"        => "Acme Trading Co.",
        "email"       => "[email protected]",
        "country"     => "US",
        "entity_type" => "company",
        "business"    => [
            "legal_name"           => "Acme Trading Co. LLC",
            "structure"            => "llc",
            "phone"                => "+12125550123",
            "url"                  => "https://acme.example",
            "product_description"  => "Imported kitchenware and small appliances.",
            "industry_code"        => "5722",
            "statement_descriptor" => "ACME TRADING",
            "address"              => [
                "line1"       => "1600 Market St",
                "city"        => "Philadelphia",
                "state"       => "PA",
                "postal_code" => "19103",
                "country"     => "US",
            ],
        ],
    ]),
]);

$response = curl_exec($ch);
$status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status . PHP_EOL;
echo $response . PHP_EOL;

The response includes "id": "mrc_…" and "status": "created", and an empty processors list. A merchant on its own can't take money yet. Attach a processor next.응답에는 "id": "mrc_…""status": "created", 그리고 빈 processors 목록이 포함됩니다. 가맹점만 생성해서는 아직 결제를 받을 수 없습니다. 다음으로 결제 처리사를 연결해 주십시오.

Attach a processor결제 처리사 연결

A merchant with no processor has nowhere to take money. Attaching one opens its connected account with the processor and becomes the merchant's default_processor, used whenever a charge names none.결제 처리사가 연결되지 않은 가맹점은 결제를 받을 수단이 없습니다. 처리사를 연결하면 해당 가맹점의 연결 계정이 개설되며, 이 처리사가 가맹점의 default_processor가 되어 결제 시 처리사를 지정하지 않으면 사용됩니다.

This is also where you send the merchant's tax_id and representative if you already hold them. We forward both to the processor and store neither. Combined with the business details you sent at creation, it takes a US company's onboarding from 15 outstanding requirements down to 4.이미 보유하고 계신 가맹점의 tax_id와 representative도 이 단계에서 전달하십시오. 두 값 모두 결제 처리사에 전달만 하고 저장하지 않습니다. 가맹점 생성 시 보내신 사업 정보와 함께 사용하면 미국 법인의 온보딩 미충족 요건이 15개에서 4개로 줄어듭니다.

<?php
$ch = curl_init("https://api.outlinepay.com/v1/merchants/mrc_9f2c8bAaZ1kQ/processors");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer olp_sk_test_YOUR_KEY",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        "tax_id"         => "00-0000000",
        "representative" => [
            "first_name"    => "Dana",
            "last_name"     => "Whitfield",
            "email"         => "[email protected]",
            "phone"         => "+12125550188",
            "date_of_birth" => "1985-04-27",
            "address"       => [
                "line1"       => "220 Spruce St",
                "city"        => "Philadelphia",
                "state"       => "PA",
                "postal_code" => "19106",
                "country"     => "US",
            ],
        ],
    ]),
]);

$response = curl_exec($ch);
$status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status . PHP_EOL;
echo $response . PHP_EOL;

The merchant comes back with a processor whose onboarding_completed and verified are both false, and status still "created". That is expected, not a failure: the account is open but restricted. Onboarding is what lifts the restriction.응답의 가맹점에는 onboarding_completedverified가 모두 false인 처리사가 포함되고, status는 여전히 "created"입니다. 이는 오류가 아니라 정상입니다. 계정은 개설되었지만 제한된 상태이며, 이 제한은 온보딩으로만 해제됩니다.

Onboard the merchant가맹점 온보딩

The processor will not let a restricted account take money until the merchant has completed its verification, and that only happens inside an embedded component rendered on a page you host. There is no hosted link you can email: the component runs on your own site, under your own login.결제 처리사는 가맹점이 검증을 완료하기 전까지 제한된 계정으로 결제를 받도록 허용하지 않습니다. 그리고 이 검증은 귀사가 호스팅하는 페이지에 삽입된 임베드 컴포넌트 안에서만 완료됩니다. 이메일로 보낼 수 있는 호스팅 링크는 없습니다. 컴포넌트는 귀사의 사이트에서, 귀사의 로그인 세션 아래에서 동작합니다.

Before starting, have the merchant check their business website. Verification requires a working site (or a complete, public social profile or app listing) showing the business name, what it sells, contact details, and a refund policy, and activation stalls until it passes (Stripe's website requirements).시작하기 전에 가맹점의 웹사이트를 점검하도록 안내해 주십시오. 검증 과정에서는 상호, 판매 품목, 연락처, 환불 정책이 표시된 실제 운영 중인 웹사이트(또는 완전하게 공개된 소셜 프로필이나 앱 페이지)를 확인하며, 통과할 때까지 활성화가 보류됩니다(Stripe 웹사이트 요건).

Which machine does what어느 기기가 무엇을 하는가

Who calls whom누가 누구를 호출하는가
YOUR BROWSER           YOUR SERVER         OUTLINEPAY        STRIPE
outlinepay.js          holds olp_sk_       holds the
+ your page                                Stripe keys
    |                      |                   |               |
    |  POST /merchant/session                  |               |
    |  (your route, not ours)                  |               |
    |--------------------->|                   |               |
    |                      |  POST /v1/merchants/{id}/account_session
    |                      |  Authorization: Bearer olp_sk_    |
    |                      |------------------>|               |
    |                      |                   | create the    |
    |                      |                   | session       |
    |                      |                   |-------------->|
    |  the session body,   |<------------------|               |
    |  passed through      |                   |               |
    |<---------------------|                   |               |
    |                                                          |
    |  outlinepay.js loads connect.js, then mounts the component
    |--------------------------------------------------------->|

outlinepay.js NEVER calls /v1. It cannot: minting a session needs your
secret key, so that call stays on your server. The only request it makes
to a host of ours is the one that downloaded the file.
귀사의 브라우저귀사의 서버OUTLINEPAYSTRIPE
outlinepay.jsolp_sk_ 보관Stripe 키 보관
+ 귀사의 페이지
    |                      |                   |               |
    |  POST /merchant/session                  |               |
    |  (당사가 아닌 귀사의 라우트)|               |
    |--------------------->|                   |               |
    |                      |  POST /v1/merchants/{id}/account_session
    |                      |  Authorization: Bearer olp_sk_    |
    |                      |------------------>|               |
    |                      |                   | 세션 생성|
    |                      |                   |               |
    |                      |                   |-------------->|
    |  세션 본문을|<------------------|               |
    |  그대로 전달|                   |               |
    |<---------------------|                   |               |
    |                                                          |
    |  outlinepay.js가 connect.js를 불러와 컴포넌트를 마운트합니다
    |--------------------------------------------------------->|

outlinepay.js는 /v1을 절대 호출하지 않습니다. 호출할 수도 없습니다. 세션을
발급하려면 귀사의 시크릿 키가 필요하므로 그 호출은 귀사의 서버에 남습니다.
이 파일이 당사 호스트에 보내는 요청은 파일을 내려받은 그 한 번뿐입니다.

Start with the OutlinePay call on its own. Run it from a terminal to see the shape of what comes back: a short-lived client_secret and the publishable key the component needs.먼저 OutlinePay 호출만 단독으로 실행해 보십시오. 터미널에서 실행하시면 반환되는 형태를 확인하실 수 있습니다. 유효 기간이 짧은 client_secret과 컴포넌트에 필요한 공개 키가 반환됩니다.

1 · The call itself1 · 호출 자체

<?php
$ch = curl_init("https://api.outlinepay.com/v1/merchants/mrc_9f2c8bAaZ1kQ/account_session");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer olp_sk_test_YOUR_KEY",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        "components" => [
            "onboarding",
        ],
    ]),
]);

$response = curl_exec($ch);
$status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status . PHP_EOL;
echo $response . PHP_EOL;

Now wrap that call in a route on your own server. It is the only endpoint your page talks to. It keeps your secret key on your server, and your login session, not the browser, decides which merchant the session is for.이제 이 호출을 귀사의 서버에 있는 라우트로 감싸십시오. 귀사의 페이지가 통신하는 엔드포인트는 이 라우트뿐입니다. 시크릿 키는 귀사의 서버에 남고, 어느 가맹점을 위한 세션인지는 브라우저가 아니라 귀사의 로그인 세션이 결정합니다.

2 · Your server2 · 귀사의 서버

<?php
// YOUR route, on YOUR server. Your page calls this one.
// It is not an OutlinePay endpoint, and OutlinePay never calls it.
//   POST https://pg.cookiepay.example/merchant/onboarding-session

// Which merchant this is for comes from YOUR login session, never from the
// request body. Reading it from the browser would let any signed-in user
// mint a session for somebody else's merchant.
$merchantId = merchant_for_current_session();

// The OutlinePay call. Your secret key is used on this machine and stays here.
$ch = curl_init("https://api.outlinepay.com/v1/merchants/{$merchantId}/account_session");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer olp_sk_test_YOUR_KEY",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS     => json_encode(["components" => ["onboarding"]]),
]);

// Hand our response back as it arrived. outlinepay.js reads what it needs
// out of it, so there is no list of fields to forward and none to forget.
//
// Nothing in that body is a secret of yours: client_secret is scoped to this
// one merchant and only the components you named, publishable_key is public,
// and processor and locale are not sensitive. Your key is not in it.
header("Content-Type: application/json");
echo curl_exec($ch);
curl_close($ch);

Then, on your page, load outlinepay.js and mount the component, pointing it at that route of yours.그다음 귀사의 페이지에서 outlinepay.js를 불러와, 방금 만드신 귀사의 라우트를 바라보도록 컴포넌트를 생성하십시오.

3 · Your page3 · 귀사의 페이지

<script src="https://js.outlinepay.com/v1/outlinepay.js"></script>

<div id="onboarding"></div>

<script>
  // The whole of the browser half. sessionUrl is YOUR route, the one above,
  // and it is the only endpoint this page knows about.
  OutlinePay.mount("onboarding", "#onboarding", {
    sessionUrl: "/merchant/onboarding-session",
  })
</script>

Load that script from https://js.outlinepay.com/v1/outlinepay.js, not from a bundled or self-hosted copy. Put the tag in your <head> so it starts loading before the page needs it.이 스크립트는 번들이나 직접 호스팅한 사본이 아니라 https://js.outlinepay.com/v1/outlinepay.js에서 불러와 주십시오. 페이지가 필요로 하기 전에 로딩이 시작되도록 태그는 <head>에 두십시오.

The file fetches the processor's own SDK for you, from connect-js.stripe.com, so a Content-Security-Policy has to allow both hosts. During onboarding it also frames the OutlinePay merchant terms, so a policy that sets frame-src has to allow the origin of terms.url from the account-session response. Everything else it does is described under outlinepay.js, including every error it can raise.이 파일은 결제 처리사의 SDK를 connect-js.stripe.com에서 대신 불러오므로, Content-Security-Policy를 설정하신 경우 두 호스트를 모두 허용해야 합니다. 온보딩 중에는 OutlinePay 가맹점 약관 화면도 iframe으로 표시하므로, frame-src를 설정하셨다면 account session 응답의 terms.url 오리진도 허용해 주십시오. 그 밖의 모든 동작과 발생 가능한 모든 오류는 outlinepay.js에 정리되어 있습니다.

Two machines, two calls서로 다른 두 기기, 두 번의 호출

Blocks 2 and 3 run on different machines. Nothing in the browser calls /v1 or holds your secret key. The route name in these samples is yours to choose.2번과 3번 블록은 서로 다른 기기에서 실행됩니다. 브라우저에서는 /v1을 호출하지도, 시크릿 키를 보유하지도 않습니다. 예시의 라우트 이름은 원하시는 대로 정하시면 됩니다.

How you know it worked: processors[].onboarding_completed turns true when the merchant submits, and processors[].verified turns true when the processor accepts them. At that moment status becomes "active" and we emit a merchant.updated event. The component's own callbacks tell you the form closed, not that the merchant was accepted.완료 확인 방법은 다음과 같습니다. 가맹점이 제출하면 processors[].onboarding_completedtrue가 되고, 결제 처리사가 승인하면 processors[].verifiedtrue가 됩니다. 이 시점에 status"active"로 바뀌고 merchant.updated 이벤트를 발송합니다. 컴포넌트 자체의 콜백은 양식이 닫혔다는 뜻일 뿐, 가맹점이 승인되었다는 뜻은 아닙니다.

Take a payment결제 받기

Once the merchant is onboarded, create a payment for it. You get back a payment in status awaiting_payment plus a checkout_url, an OutlinePay-owned hosted page. Send the buyer there to complete the payment on the processor.가맹점 온보딩이 완료되면 해당 가맹점의 결제를 생성하십시오. 응답으로 status가 awaiting_payment인 결제 객체와 함께, OutlinePay가 소유한 호스팅 페이지 주소인 checkout_url을 받으십니다. 구매자를 이 주소로 보내면 결제 처리사에서 결제를 완료합니다.

<?php
$ch = curl_init("https://api.outlinepay.com/v1/payments");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer olp_sk_test_YOUR_KEY",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        "merchant_id" => "mrc_9f2c8bAaZ1kQ",
        "amount"      => 10000,
        "currency"    => "usd",
        "description" => "Order",
        "customer"    => [
            "email" => "[email protected]",
        ],
        "success_url" => "https://pg.cookiepay.example/op/result",
        "cancel_url"  => "https://pg.cookiepay.example/op/cancel",
    ]),
]);

$response = curl_exec($ch);
$status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status . PHP_EOL;
echo $response . PHP_EOL;

Before onboarding finishes, this call and Create a payment link both refuse with 409 merchant_not_onboarded and create nothing. If you see it, the merchant has not finished onboarding. The rule is under Onboarding is required before charging.온보딩이 끝나기 전에는 이 호출과 결제 링크 생성이 모두 409 merchant_not_onboarded로 거부되며 아무것도 생성되지 않습니다. 이 오류가 보이신다면 해당 가맹점은 아직 온보딩을 마치지 않은 것입니다. 규칙은 결제 전 온보딩 필수에 정리되어 있습니다.

Redirect the buyer to the checkout_url from the response:응답에 담긴 checkout_url로 구매자를 이동시키십시오:

<?php
// $payment is the decoded create-payment response
header("Location: " . $payment["checkout_url"]);
exit;

On your test key the checkout accepts only the processor's test cards. A real card is refused. Pay with 4242 4242 4242 4242, any future expiry, any CVC, any name and postal code. For declines, 3D Secure, disputes and the other scenarios, see the Testing page.테스트 키에서는 체크아웃이 결제 처리사의 테스트 카드만 받으며, 실제 카드는 거부됩니다. 카드 번호 4242 4242 4242 4242에 미래의 아무 유효기간, 아무 CVC, 아무 이름과 우편번호로 결제해 보십시오. 거절, 3D Secure, 분쟁 등 다른 시나리오는 테스트 페이지를 참고해 주십시오.

Important

Creating a payment is never a payment outcome. The redirect back to your success_url is navigation, not proof of payment. The terminal state (succeeded / failed / expired) arrives by webhook. Never fulfill an order on the redirect alone.결제 생성은 결제 결과가 아닙니다. success_url로 돌아오는 리디렉션은 화면 이동일 뿐 결제가 완료되었다는 증거가 아닙니다. 최종 상태(succeeded / failed / expired)는 웹훅으로 전달됩니다. 리디렉션만 보고 주문을 처리하시면 안 됩니다.

Get the result결과 확인

Register a webhook endpoint once, and OutlinePay will POST a signed event to it whenever a payment settles (payment.succeeded, payment.failed, payment.expired, and more). This is the source of truth. Retrieve a payment is available as a polling fallback.웹훅 엔드포인트를 한 번 등록해 두시면, 결제가 완료될 때마다 OutlinePay가 서명된 이벤트를 POST로 전달합니다(payment.succeeded, payment.failed, payment.expired 등). 이것이 기준 정보이며, 결제 조회는 폴링 대체 수단으로 제공됩니다.

<?php
$ch = curl_init("https://api.outlinepay.com/v1/webhook_endpoints");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer olp_sk_test_YOUR_KEY",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        "url" => "https://pg.cookiepay.example/outlinepay/webhook",
    ]),
]);

$response = curl_exec($ch);
$status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status . PHP_EOL;
echo $response . PHP_EOL;

The response returns the endpoint's secret (olp_whsec_…) exactly once. Store it now. You'll need it to verify deliveries in the next step.응답에는 엔드포인트의 secret(olp_whsec_…)이 단 한 번만 포함됩니다. 지금 저장해 주십시오. 다음 단계에서 전달 건을 검증하실 때 필요합니다.

Webhooks only reach a public HTTPS URL, so they cannot land on localhost. While developing, put a tunnel in front of your dev server (ngrok, cloudflared) and register the tunnel's URL as the endpoint. Or skip webhooks for now and poll Retrieve a payment: it shows the same result, a moment later.웹훅은 공개된 HTTPS URL로만 전달되므로 localhost에는 도달하지 않습니다. 개발 중에는 개발 서버 앞에 터널(ngrok, cloudflared 등)을 두고 터널 URL을 엔드포인트로 등록해 주십시오. 아니면 당분간 웹훅 없이 결제 조회를 폴링하셔도 됩니다. 같은 정보가 잠시 늦게 도착할 뿐입니다.

Verify a webhook웹훅 검증

Every delivery carries an OutlinePay-Signature header: t=<unix>,v1=<HMAC-SHA256 of "timestamp.body" keyed by your signing secret>. Recompute it and compare before trusting the payload. An unverified webhook is just an untrusted POST from the internet.모든 전달 건에는 OutlinePay-Signature 헤더가 포함됩니다. 형식은 t=<unix>,v1=<서명 시크릿을 키로 "timestamp.body"에 대해 계산한 HMAC-SHA256>입니다. 페이로드를 신뢰하시기 전에 직접 다시 계산해 비교해 주십시오. 검증하지 않은 웹훅은 인터넷에서 들어온 신뢰할 수 없는 POST일 뿐입니다.

<?php
// Verify an OutlinePay webhook before trusting it.
// Header: OutlinePay-Signature: t=<unix>,v1=<HMAC-SHA256(secret, "t.body")>
$secret  = "olp_whsec_YOUR_SIGNING_SECRET";          // shown once when you create the endpoint
$payload = file_get_contents("php://input");         // the RAW body. Do not json_decode first
$header  = $_SERVER["HTTP_OUTLINEPAY_SIGNATURE"] ?? "";

// Pull t= and v1= out of the header.
$parts = [];
foreach (explode(",", $header) as $pair) {
    [$k, $v] = array_pad(explode("=", $pair, 2), 2, "");
    $parts[$k] = $v;
}
$timestamp = (int)($parts["t"] ?? 0);
$given     = $parts["v1"] ?? "";

// The timestamp is when this delivery attempt was sent, not when the event
// was created. Retries are re-signed fresh, so late ones still verify.
// Reject a timestamp older than 5 minutes to stop replayed deliveries.
if ($timestamp === 0 || abs(time() - $timestamp) > 300) {
    http_response_code(400);
    exit("stale or malformed signature");
}

// Recompute the HMAC over "timestamp.body" and compare in constant time.
$expected = hash_hmac("sha256", $timestamp . "." . $payload, $secret);
if (!hash_equals($expected, $given)) {
    http_response_code(400);
    exit("signature mismatch");
}

// Verified. Handle the event, then return 2xx so OutlinePay stops retrying.
$event = json_decode($payload, true);
// switch ($event["type"]) { case "payment.succeeded": ... }
http_response_code(200);

How delivery behaves around this handler, the timeout, the retry schedule, duplicates, and catching up on missed events, is under Webhook delivery in the reference.이 핸들러를 둘러싼 전달 동작, 즉 타임아웃, 재시도 일정, 중복 전달, 놓친 이벤트를 따라잡는 방법은 레퍼런스의 웹훅 전달에 정리되어 있습니다.

The rest of the components나머지 컴포넌트

Onboarding is not the only component you host. Six more mount the same way: your route mints a session, your page hands it to the SDK. Everything else a merchant does (taking payments, issuing refunds, choosing which payment methods to accept) is served over /v1, and you build that UI however you like.호스팅하셔야 하는 컴포넌트는 온보딩만이 아닙니다. 여섯 개가 더 있으며 마운트 방식은 같습니다. 귀사의 라우트가 세션을 발급하고, 귀사의 페이지가 이를 SDK에 전달합니다. 그 밖에 가맹점이 하는 일(결제 수납, 환불 처리, 허용할 결제 수단 선택)은 /v1으로 제공되므로 UI는 원하시는 대로 구현하시면 됩니다.

Ask us forOutlinePay에 요청하는 값 Mount as마운트할 이름 Why it's a component왜 컴포넌트인가
onboardingaccount_onboardingStripe requires it. Nothing charges until it's done.Stripe 필수. 완료 전에는 결제가 되지 않습니다.
account_managementaccount_managementStripe requires it, and it's the only place a merchant can fix its own verification details or change its bank account.Stripe 필수이며, 가맹점이 자신의 검증 정보를 수정하거나 계좌를 변경할 수 있는 유일한 곳입니다.
notificationsnotification_bannerStripe requires it. This is how Stripe asks a merchant for something, and the only place the merchant can answer.Stripe 필수. Stripe가 가맹점에 무언가를 요청하는 통로이자, 가맹점이 답할 수 있는 유일한 곳입니다.
balancesbalancesThe only way a merchant can put money into its balance to cover refunds and disputes. There is no API for it.가맹점이 환불과 분쟁에 대비해 잔액에 자금을 넣을 수 있는 유일한 방법입니다. 이 기능에는 API가 없습니다.
documentsdocumentsTax forms and the Stripe invoices for the fees the merchant paid. Both are issued by Stripe straight to the merchant.세금 서식과 가맹점이 납부한 수수료의 Stripe 인보이스. 둘 다 Stripe가 가맹점에 직접 발행합니다.
disputesdisputes_listResponding to a chargeback means uploading evidence. Building that over an API is far more work.지불 거절 대응은 증빙 업로드를 뜻합니다. API로 직접 구현하면 훨씬 더 많은 작업이 듭니다.
payoutspayoutsPayout history, and the payout schedule. The schedule has no API at all, so without this the merchant can't choose when it gets paid.지급 내역과 지급 주기. 지급 주기에는 API가 전혀 없어서, 이 컴포넌트가 없으면 가맹점은 지급 시점을 정할 수 없습니다.

You only type the first name, in components on the account-session call and in mount(). The second column is the processor's own id for the same surface, which outlinepay.js translates for you. You will see those ids in the processor's dashboard and support replies. The same request and mount keep working when a merchant is on a different processor.귀사가 입력하시는 것은 첫 번째 이름뿐이며, 계정 세션 호출의 components와 mount()에 쓰입니다. 두 번째 열은 같은 화면에 대한 결제 처리사 자체의 식별자이고, outlinepay.js가 대신 변환합니다. 이 식별자는 처리사의 대시보드와 지원 답변에서 보시게 됩니다. 가맹점이 다른 결제 처리사에 있어도 같은 요청과 같은 mount가 계속 동작합니다.

Setting one up is the same three blocks as onboarding, in the same order, and the difference is one line in each: name the components this page needs instead of onboarding, and mount one per slot. A worked example for a page showing a merchant its balance and its payouts.설정 방법은 온보딩과 같은 세 블록, 같은 순서이며, 각 블록에서 한 줄씩만 달라집니다. onboarding 대신 해당 페이지에 필요한 컴포넌트를 지정하시고, 슬롯마다 컴포넌트를 하나씩 생성하시면 됩니다. 아래는 가맹점에 잔액과 지급 내역을 보여 주는 페이지의 예시입니다.

1 · The call itself1 · 호출 자체

curl -X POST "https://api.outlinepay.com/v1/merchants/mrc_9f2c8bAaZ1kQ/account_session" \
  -H "Authorization: Bearer olp_sk_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "components": ["balances", "payouts"]
}'

Then the same route on your server, widened so one route serves every component page you build. Which components a page may ask for is decided here, on your server, not by the browser.이어서 귀사의 서버에 두는 같은 라우트이며, 앞으로 만드실 모든 컴포넌트 페이지를 하나의 라우트로 처리할 수 있도록 조금 넓혔습니다. 어떤 페이지가 어떤 컴포넌트를 요청할 수 있는지는 브라우저가 아니라 귀사의 서버에서 결정합니다.

2 · Your server2 · 귀사의 서버

<?php
// YOUR route again, one step wider, so the same one serves every component
// page you build.
//   POST https://pg.cookiepay.example/merchant/session

// Still your login session that decides the merchant. Nothing changed here.
$merchantId = merchant_for_current_session();

// Which page is asking, and what that page is allowed to mount. An
// allowlist, not a pass-through: if the browser could name its own
// components, a payout page could ask for a credential that also opens
// account management.
$pages = [
    "money"    => ["balances", "payouts"],
    "account"  => ["account_management", "notifications"],
    "disputes" => ["disputes"],
    "tax"      => ["documents"],
];

$body = json_decode(file_get_contents("php://input"), true) ?: [];
$page = $body["page"] ?? "";
if (!isset($pages[$page])) {
    http_response_code(400);
    exit("unknown page");
}

$ch = curl_init("https://api.outlinepay.com/v1/merchants/{$merchantId}/account_session");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer olp_sk_test_YOUR_KEY",
        "Content-Type: application/json",
    ],
    // The only line that differs from the onboarding route.
    CURLOPT_POSTFIELDS     => json_encode(["components" => $pages[$page]]),
]);

// Passed straight through, exactly as in the onboarding route.
header("Content-Type: application/json");
echo curl_exec($ch);
curl_close($ch);

Then your page: one mount call per slot. That is all that changes between component pages.그다음 귀사의 페이지입니다. 슬롯마다 mount 호출 하나입니다. 컴포넌트 페이지 사이에서 달라지는 것은 이것이 전부입니다.

3 · Your page3 · 귀사의 페이지

<script src="https://js.outlinepay.com/v1/outlinepay.js"></script>

<div id="balances"></div>
<div id="payouts"></div>

<script>
  // Both name the same sessionUrl and the same request, so they share one
  // account session and one SDK instance between them. Ask for a different
  // page and you get a second of each, which is why a page's components
  // belong in a single request.
  const options = {
    sessionUrl: "/merchant/session",   // yours, not ours
    request: { page: "money" },        // your server decides what this may ask for
  }

  // Chained, not fired side by side, and it matters: mount() checks that
  // shared instance BEFORE it awaits the session, so two calls made at once
  // both miss it and each mint their own. Awaiting the first is what makes
  // the second free.
  OutlinePay.mount("balances", "#balances", options)
    .then(() => OutlinePay.mount("payouts", "#payouts", options))
</script>

Scope each session to the page. A session grants exactly what you name in components, so a page showing payouts should not ask for the other six. How you group them is up to you. The demo puts each on its own page.세션의 범위는 페이지에 맞추어 주십시오. 세션은 components에 지정하신 범위만 허용하므로, 지급 내역을 보여 주는 페이지가 나머지 여섯 개를 함께 요청해서는 안 됩니다. 묶는 방식은 자유입니다. 데모는 각각 별도 페이지에 두었습니다.

outlinepay.jsoutlinepay.js

You have now mounted three components with one call. This section is the reference for that call: what it handles on your behalf, the options you can pass it, and every error it can raise.여기까지 호출 하나로 컴포넌트 세 개를 마운트하셨습니다. 이 섹션은 그 호출의 레퍼런스입니다. 대신 처리해 주는 것, 전달할 수 있는 옵션, 발생할 수 있는 모든 오류를 다룹니다.

Using it changes nothing about the integration. Your secret key stays on your server, your login session still decides which merchant a session is for, and your server still decides which components a page may ask for. It moves the browser code out of your repository into a file we maintain.이 파일을 사용해도 연동의 구조는 바뀌지 않습니다. 시크릿 키는 귀사의 서버에 남고, 어느 가맹점을 위한 세션인지는 여전히 귀사의 로그인 세션이 결정하며, 어떤 페이지가 어떤 컴포넌트를 요청할 수 있는지도 여전히 귀사의 서버가 결정합니다. 브라우저 코드가 귀사의 저장소에서 OutlinePay가 관리하는 파일로 옮겨질 뿐입니다.

What it is doing for you대신 처리해 주는 것

The rule규칙 What happens if you miss it놓쳤을 때
Pass the merchant's locale to the SDK가맹점의 locale을 SDK에 전달 The form renders in the browser's language. A Korean merchant gets an English onboarding form and nothing errors.양식이 브라우저의 언어로 렌더링됩니다. 한국 가맹점에 영어 온보딩 양식이 표시되며 아무런 오류도 발생하지 않습니다.
One SDK instance per page, reused페이지당 SDK 인스턴스 하나를 재사용 An instance per component costs an extra account session each and slows the page. Stripe's performance guidance warns against it.컴포넌트마다 인스턴스를 만들면 매번 계정 세션이 추가로 소모되고 페이지가 느려집니다. Stripe의 성능 권장 사항도 이를 피하라고 안내합니다.
Mint a fresh session, never replay one세션을 재사용하지 않고 새로 발급 A session expires mid-form. Hand back the same secret and the merchant is stranded part-way through onboarding.세션은 양식 작성 중에 만료됩니다. 같은 secret을 다시 반환하면 가맹점이 온보딩 도중에 멈춥니다.
Translate our name to the processor's idOutlinePay 이름을 처리사 식별자로 변환 Your front end ends up pinned to one processor.귀사의 프런트엔드가 특정 결제 처리사에 묶이게 됩니다.
Check the component against the session's scope세션 범위와 컴포넌트를 대조 An empty box, and the reason never reaches your console.빈 상자가 표시되고, 원인은 콘솔에 전달되지 않습니다.

These are the errors it raises instead of a blank component.빈 컴포넌트 대신 다음과 같은 오류를 발생시킵니다.

Console
// A component the session does not cover. Without the wrapper this renders
// an empty box and the reason is swallowed inside the processor's handshake.
outlinepay.js [component_not_in_session]: the session from /merchant/session
covers [balances, payouts], so it cannot mount disputes. Add disputes to the
components your endpoint asks for.

// A route that picked fields out of our response instead of passing it on.
outlinepay.js [endpoint_missing_field]: your endpoint /merchant/session
returned no publishable_key. Return the account-session response body as we
sent it to you.

// A Content-Security-Policy that does not allow the processor's SDK.
outlinepay.js [processor_sdk_blocked]: could not load
https://connect-js.stripe.com/v1.0/connect.js. If your Content-Security-Policy
sets script-src, it has to allow https://connect-js.stripe.com.

Every error, by code오류 코드 전체

The name in brackets is the stable part of the message and is on the error object as err.code. Branch on the code, not the prose, which may be reworded. Messages are English because one file is served to every partner. This table gives the explanation in your language.대괄호 안의 이름은 메시지에서 바뀌지 않는 부분이며, 오류 객체의 err.code로도 제공됩니다. 문구는 바뀔 수 있으므로 문구가 아니라 코드로 분기해 주십시오. 모든 파트너에게 같은 파일이 제공되므로 메시지는 영어입니다. 이 표가 한국어 설명을 제공합니다.

Code코드 What it means의미
unknown_component The first argument is not one of our component names. The message lists the ones that are.첫 번째 인자가 OutlinePay의 컴포넌트 이름이 아닙니다. 사용 가능한 이름은 오류 메시지에 함께 표시됩니다.
session_url_missing mount() was called without sessionUrl, so there is no endpoint of yours to mint a session from.mount()를 sessionUrl 없이 호출하셨습니다. 세션을 발급받을 귀사의 엔드포인트가 지정되지 않았습니다.
session_url_invalid sessionUrl is not a URL. A path such as /merchant/session is fine; an empty string or an object is not.sessionUrl이 URL 형식이 아닙니다. /merchant/session과 같은 경로는 괜찮지만, 빈 문자열이나 객체는 사용하실 수 없습니다.
target_not_found No element on the page matched the selector you passed. Usually the script ran before the element existed.전달하신 선택자와 일치하는 요소가 페이지에 없습니다. 대개 해당 요소가 만들어지기 전에 스크립트가 실행된 경우입니다.
credentials_invalid credentials has to be "omit" or "include". A misspelling is refused here because fetch would silently treat it as its default.credentials 값은 "omit" 또는 "include"여야 합니다. 오타를 여기에서 거부하는 이유는, fetch가 알 수 없는 값을 조용히 기본값으로 처리하기 때문입니다.
headers_invalid headers has to be an object, or a function returning one. A string is not headers.headers는 객체이거나 객체를 반환하는 함수여야 합니다. 문자열은 헤더가 아닙니다.
cross_origin_not_allowed sessionUrl is on another origin and crossOrigin: true was not passed. Deliberate, so that a URL built from a query parameter cannot redirect who supplies the session.sessionUrl이 다른 오리진에 있는데 crossOrigin: true가 전달되지 않았습니다. 의도된 동작입니다. 쿼리 파라미터로 조립된 URL이 세션을 제공하는 주체를 다른 곳으로 바꿀 수 없도록 하기 위한 것입니다.
endpoint_unreachable The request to your own route never completed. A wrong path, a CORS refusal, or the page being offline.귀사의 라우트로 보낸 요청이 완료되지 않았습니다. 잘못된 경로, CORS 거부, 또는 페이지가 오프라인인 경우입니다.
endpoint_http_error Your route answered with an error status. Often a login the request did not carry, or a CSRF check: pass the token as headers rather than exempting the route.귀사의 라우트가 오류 상태 코드로 응답했습니다. 요청이 로그인 세션을 전달하지 못한 경우가 많고, CSRF 검사인 경우도 많습니다. 후자라면 라우트를 예외 처리하지 마시고 토큰을 headers로 전달해 주십시오.
endpoint_not_json Your route answered with something that is not JSON, which usually means an HTML error page.귀사의 라우트가 JSON이 아닌 응답을 반환했습니다. 대개 HTML 오류 페이지가 반환된 경우입니다.
endpoint_missing_field Your route answered with JSON that is missing a field the account-session response has. Return our response body as it arrived rather than picking fields out of it.귀사의 라우트가 반환한 JSON에 계정 세션 응답의 필드 하나가 빠져 있습니다. 응답에서 필드를 골라내지 마시고 OutlinePay의 응답 본문을 그대로 반환해 주십시오.
component_not_in_session The session your route minted does not cover the component you asked to mount. Add it to the components your route requests for that page. This is the one that renders an empty box without the wrapper.귀사의 라우트가 발급한 세션이 요청하신 컴포넌트를 포함하지 않습니다. 해당 페이지의 라우트가 요청하는 components에 추가해 주십시오. 래퍼가 없을 때 빈 상자로 표시되는 실패가 바로 이것입니다.
processor_unsupported The session names a processor this version of outlinepay.js cannot mount components for.세션에 지정된 결제 처리사는 이 버전의 outlinepay.js가 컴포넌트를 마운트할 수 없는 처리사입니다.
processor_sdk_blocked The processor's own SDK did not load. Nearly always a Content-Security-Policy whose script-src does not allow it.결제 처리사의 SDK가 로드되지 않았습니다. 거의 항상 Content-Security-Policy의 script-src가 해당 호스트를 허용하지 않은 경우입니다.
terms_frame_blocked The merchant-terms screen never reported in. Nearly always a Content-Security-Policy whose frame-src does not allow the origin of terms.url in the account-session response. Logged rather than thrown, because a slow network looks identical.가맹점 약관 화면이 응답하지 않았습니다. 거의 항상 Content-Security-Policy의 frame-src가 account session 응답의 terms.url 오리진을 허용하지 않은 경우입니다. 느린 네트워크와 구분할 수 없으므로 오류로 발생시키지 않고 로그로만 남깁니다.
terms_session_expired The merchant-terms URL expired twice in a row, which means the frame could not be refreshed with a working one. Mint a fresh account session and mount the onboarding component again.가맹점 약관 URL이 두 번 연속 만료되어 프레임을 유효한 URL로 새로고침할 수 없었습니다. account session을 새로 발급하고 onboarding 컴포넌트를 다시 마운트해 주세요.

Two options for ordinary stacks일반적인 스택을 위한 두 가지 옵션

If your framework checks a CSRF token on POST, pass it as headers rather than exempting the route: headers: { "X-CSRF-Token": token }.프레임워크가 POST에서 CSRF 토큰을 검사한다면, 해당 라우트를 예외 처리하지 마시고 토큰을 headers로 전달해 주십시오. 예: headers: { "X-CSRF-Token": token }.

If the page and the session route live on different origins, pass crossOrigin: true. It is refused by default because sessionUrl decides who hands the browser its credentials, and a URL built from a query parameter should not be able to point that somewhere else. The route's server also has to allow your page's origin with Access-Control-Allow-Origin.페이지와 세션 라우트의 오리진이 다르다면 crossOrigin: true를 전달해 주십시오. 기본값으로 거부하는 이유는 sessionUrl이 브라우저에 자격 증명을 제공할 주체를 결정하기 때문입니다. 쿼리 파라미터로 조립된 URL이 그 주체를 다른 곳으로 바꿀 수 있어서는 안 됩니다. 또한 해당 라우트의 서버가 Access-Control-Allow-Origin으로 귀사 페이지의 오리진을 허용해야 합니다.

Cross-origin, your login session cannot follow. A cross-site request carries the cookies of the host being called, never those of the page calling it, however you configure CORS. Identify the merchant with a token in headers instead, and pass headers as a function so the token can expire in minutes: we call it again for every request, including the re-mints the processor asks for hours after the page loaded.오리진이 다르면 귀사의 로그인 세션은 함께 전달되지 않습니다. 크로스 사이트 요청은 호출하는 페이지가 아니라 호출받는 호스트의 쿠키를 전달하며, CORS 설정과 무관합니다. 대신 headers에 담은 토큰으로 가맹점을 식별해 주시고, 토큰이 몇 분 만에 만료될 수 있도록 headers를 함수로 전달해 주십시오. 모든 요청마다 함수를 다시 호출하며, 페이지 로드 후 몇 시간이 지나 처리사가 요청하는 재발급 시점에도 마찬가지입니다.

Two hosts in your CSP, not oneCSP에 호스트 두 개가 필요합니다

If you set a Content-Security-Policy, script-src has to allow both js.outlinepay.com and https://connect-js.stripe.com, because our file loads the processor's. Otherwise the component renders empty, with the error swallowed inside the SDK. Check this first when a component does not appear.Content-Security-Policy를 설정하신 경우, OutlinePay의 파일이 결제 처리사의 파일을 불러오므로 script-src에 js.outlinepay.com과 https://connect-js.stripe.com 두 곳을 모두 허용해야 합니다. 그렇지 않으면 컴포넌트가 빈 상태로 렌더링되고 오류는 SDK 내부에서 삼켜집니다. 컴포넌트가 표시되지 않을 때 이 부분을 먼저 확인해 주십시오.

What depending on it means의존하실 때 감수하시는 것

The file is served from a versioned path on a short cache, where v1 names the contract rather than a build. That lets us fix a failure for every partner at once.이 파일은 버전이 붙은 경로에 짧은 캐시로 제공되며, v1은 빌드가 아니라 계약을 뜻합니다. 덕분에 문제를 모든 파트너에 대해 한 번에 수정할 수 있습니다.

The trade-off: it is a live dependency, and a path that is updated in place cannot be pinned with Subresource Integrity.상충점도 있습니다. 실시간 의존성이며, 제자리에서 갱신되는 경로는 Subresource Integrity로 고정할 수 없습니다.

If you would rather hold every processor call in your own repository, you still can. Create an account session is fully documented, and the browser half is Stripe's Connect embedded components. The five rules in the table above are then yours to handle.모든 결제 처리사 호출을 귀사의 저장소 안에 직접 두고 싶으시다면 그렇게 하실 수도 있습니다. 계정 세션 생성이 모두 문서화되어 있고, 브라우저 측은 Stripe의 Connect 임베드 컴포넌트입니다. 이 경우 위 표의 다섯 가지 규칙은 OutlinePay가 아니라 귀사가 직접 지켜야 합니다.

Go live라이브 전환

When your test flow works end to end, going live is a small set of swaps. The API surface is identical.테스트 흐름이 처음부터 끝까지 정상 동작하면, 라이브 전환은 몇 가지만 교체하시면 됩니다. API 구조는 완전히 동일합니다.

  • Swap your test key (olp_sk_test_…) for your live key (olp_sk_live_…).테스트 키(olp_sk_test_…)를 라이브 키(olp_sk_live_…)로 교체하십시오.
  • Register a webhook endpoint pointing at your production URL, and verify with its live signing secret.운영 URL을 가리키는 웹훅 엔드포인트를 등록하시고, 해당 라이브 서명 시크릿으로 검증해 주십시오.
  • Confirm each merchant has finished onboarding to the live processor before charging real customers.실제 고객에게 결제를 받으시기 전에, 각 가맹점이 라이브 결제 처리사 온보딩을 완료했는지 확인해 주십시오.

Full endpoint details, parameters, and response shapes live in the API Reference.엔드포인트 상세, 파라미터, 응답 형식은 API 레퍼런스에 정리되어 있습니다.