Signing Baskets & Drafts
This API is in beta and may change based on feedback. The endpoints, request/response shapes, and behavior described here are subject to revision in future releases. We welcome your input — please open an issue on GitHub to share feedback or report problems.
0.6.0-beta introduced a breaking rename of the draft and signing-basket status enums to align them with the underlying payment status vocabulary — see the changelog for the full before/after mapping. Signing basket behavior is unchanged in 0.7.0-beta.
Payment Type Roadmap
Draft support is being rolled out per payment type. The table below shows the current status and planned timeline.
| Payment Type | Status | Details |
|---|---|---|
| SE Bank Giro | Available | Fully supported |
| SE Plus Giro | Available | Fully supported |
| Domestic Credit Transfer | Planned | Expected in the coming months |
| DK Payment Slip (GIRO/FI) | Planned | Further in the future |
Drafts
Drafts provide a staged payment preparation workflow. Instead of submitting a fully populated payment in a single request, you can:
- Create a draft with only a name
- Update the draft incrementally as payment details become available
- Approve the draft through the
POST /payments/drafts/approveendpoint, which initiates the actual payment
Drafts do not have individual approve endpoints — all approval goes through the general Signing Baskets batch approval flow, even when approving a single draft. This is useful when payment details are collected across multiple steps or screens in your application.
Draft Lifecycle
Draft Endpoints
Each supported payment type has its own set of draft endpoints under /payments/drafts/{payment-type}/. The operations are the same across payment types, but the required fields and validation rules differ depending on the payment type (see the payment-type-specific sections below).
| Operation | Method | Path | Description |
|---|---|---|---|
| Create draft | POST | /payments/drafts/{payment-type} | Create a new draft |
| Get draft | GET | /payments/drafts/{payment-type}/{id} | Get draft details and validation status |
| Update draft | PATCH | /payments/drafts/{payment-type}/{id} | Partially update a draft |
| Delete draft | DELETE | /payments/drafts/{payment-type}/{id} | Delete a draft in CREATED status |
Drafts are approved through the Signing Baskets batch approval flow.
All draft endpoints require the PSP_PI (Payment Initiation) scope.
Common Fields
Every draft has the following fields regardless of payment type:
| Field | Type | Required on Create | Description |
|---|---|---|---|
draftId | string | No | Client-provided draft identifier. Must be a valid UUID and must not already be used. Once used, a draftId cannot be reused — even after the draft has been deleted or executed. If omitted, the server generates one. Conflicts return 409 INVALID_STATE. |
name | string | Yes | Descriptive name for the draft (e.g. bill filename, receiver) |
The redirectUrl for Strong Customer Authentication is supplied on the batch approve request, not on the draft itself.
Additional fields are payment-type-specific — see the sections below.
Draft Response
All draft endpoints return a response with this common structure:
| Field | Type | Always Present | Description |
|---|---|---|---|
draftId | string | Yes | Unique draft identifier |
status | string | Yes | Current draft status (see Draft Statuses) |
isValid | boolean | Yes | Whether the draft has all required fields for approval |
name | string | Yes | Descriptive name for the draft |
validationErrors | array | No | Present when isValid is false; describes why the draft isn’t approvable |
Payment-type-specific fields (e.g. giroNumber, amount) are included when set.
The validationErrors array currently contains a single aggregated entry — not a per-field breakdown:
| Field | Type | Description |
|---|---|---|
field | string | Always "draft" in the current API version |
message | string | Localized, human-readable reason the draft isn’t yet approvable (honors the X-Language header) |
Example — single missing field (a SE Giro draft with no giroNumber):
{
"draftId": "1f4a9c3e-2b7d-4e8a-9f10-6c8b3d5e7f01",
"status": "CREATED",
"isValid": false,
"name": "Invoice 123",
"validationErrors": [
{
"field": "draft",
"message": "Giro number is missing"
}
]
}Example — several fields missing (message is aggregated, not one entry per field):
{
"draftId": "1f4a9c3e-2b7d-4e8a-9f10-6c8b3d5e7f01",
"status": "CREATED",
"isValid": false,
"name": "Invoice 123",
"validationErrors": [
{
"field": "draft",
"message": "Several required fields are missing"
}
]
}The exact message wording is subject to change and is localized via the
X-Language header. Treat it as display text — do not parse it
programmatically.
Draft Statuses
| Status | Description |
|---|---|
CREATED | Draft is being prepared and can be updated or deleted. The draft also stays in CREATED while a basket approval is awaiting Strong Customer Authentication, and returns here when a previous payment attempt failed — paymentError is populated and the draft can be retried. |
PENDING | Approval has succeeded and the resulting payment is in progress — covers awaiting clearing and awaiting settlement for future-dated payments. |
COMPLETED | Payment has settled successfully. |
DELETED | Draft has been deleted. |
A draft can only be updated or deleted while in CREATED status. If approval
or execution fails, the draft returns to CREATED status so it can be
corrected and re-approved.
When a payment fails after approval, the draft returns to an approvable state
and reappears in GET /payments/drafts with the paymentError field
populated. This field contains a localized error message describing why the
payment failed (honoring the X-Language header). The TPP can display this
error to the user and allow them to re-approve the draft.
Error Handling
All draft endpoints return errors in a consistent format:
| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code |
message | string | Human-readable error message |
errors | array | List of validation errors |
Common error codes:
| Status | Code | Description |
|---|---|---|
400 | INVALID_DRAFT_ID | The provided draftId is not a valid UUID |
400 | INVALID_EXECUTION_DATE | date is not in the expected format (YYYY-MM-DD or RFC3339) |
403 | ACCOUNT_ACCESS_DENIED | The caller does not have access to the senderAccountId supplied on create or update (see Sender Account Access) |
404 | NOT_FOUND | Draft not found |
409 | INVALID_STATE | Draft is not in the expected state for this operation, or the supplied draftId is already in use on create |
422 | Validation error in request body | |
500 | Internal server error |
Errors specific to the batch approve flow (ALREADY_USED, MISSING_DRAFT_IDS, MISSING_REDIRECT_URL, TRANSFER_DENIED) are documented on the Approve Drafts endpoint.
SE Bank Giro & SE Plus Giro Drafts
SE Bank Giro and SE Plus Giro share the same draft fields and validation rules. The only difference is the endpoint path: /payments/drafts/se-bank-giro vs /payments/drafts/se-plus-giro.
Payment-Type Fields
In addition to the common fields, SE Giro drafts support:
| Field | Type | Description |
|---|---|---|
giroNumber | string | The giro number of the recipient |
ocrReference | string | OCR payment reference |
amount | string | Payment amount as decimal string (e.g. "100.00") |
currency | string | Currency code (SEK) |
senderAccountId | string | Sender’s account UUID |
messageToSender | string | Note to the debtor, visible on the final transaction |
date | string | Execution date (ISO-8601 date, YYYY-MM-DD) |
All of these fields are optional at creation time and can be added later via PATCH.
Validation Requirements
A SE Giro draft must have the following fields populated before it can be approved (isValid: true):
| Field | Requirement |
|---|---|
giroNumber | Must be non-empty |
amount | Must be a positive value |
currency | Must be SEK |
senderAccountId | Must be a valid account UUID |
If any of these fields are missing or invalid, the draft response will include isValid: false and a validationErrors array describing what needs to be fixed.
Sender Account Access
When senderAccountId is supplied on create (POST) or update (PATCH), Lunar verifies that the caller has access to that account. Access is granted when any of the following holds:
- The caller is the direct owner of the account (private accounts).
- The caller is a party on the organisation that owns the account (organisation/business accounts).
- The caller has been granted explicit shared access to the account by its owner.
If none of these apply, the request is rejected with 403 ACCOUNT_ACCESS_DENIED and no change is made to the draft.
Automatic clearing of senderAccountId
A draft’s senderAccountId can become unusable after the draft is created — for example when the account is closed, ownership changes, or a shared-access grant is revoked. When this happens, Lunar automatically clears senderAccountId on affected drafts that are still in CREATED status. The draft reverts to isValid: false with a validation error indicating the sender account is missing, and will continue to appear in GET /payments/drafts so the TPP can prompt the user to pick a new sender account before re-approving.
| Event | Effect on drafts |
|---|---|
| Shared account access revoked | senderAccountId cleared on drafts owned by the user whose access was revoked |
| Account ownership changed | senderAccountId cleared on drafts owned by the former owner |
| Account closed | senderAccountId cleared on all drafts referencing the closed account |
Drafts that have already left CREATED status (PENDING, COMPLETED) are
not modified by these events — only drafts still being prepared are cleared.
Example: SE Bank Giro Draft
Future payment types (e.g. Domestic Credit Transfer) will have their own fields and validation requirements. The draft lifecycle and endpoints follow the same pattern, but the specific fields will differ.
Signing Baskets (Batch Approval)
A signing basket lets you group multiple drafts — potentially across different payment types — and approve them all at once through a single authorization flow. This is particularly valuable when a user needs to pay several invoices in one go: each invoice is a separate draft, and the batch approval triggers a single authentication challenge for the entire set.
Signing Basket Flow
List Drafts
GET /payments/draftsReturns all drafts for the current user that are still approvable, across all payment types that support drafts (see the Payment Type Roadmap). Drafts in CREATED are returned — including drafts that are not yet valid (isValid: false) and drafts whose previous payment failed and are awaiting retry (paymentError set). Drafts in PENDING, COMPLETED, or DELETED are excluded.
Pass the X-Language header (supported values: en, sv, da, nb) to receive localized values for invalidReason and paymentError, and for the fallback name when a draft was created without one. TPP-supplied name values are returned as-is. Defaults to en if omitted or unrecognized.
Response:
| Field | Type | Description |
|---|---|---|
drafts | array | List of draft summaries |
totalCount | integer | Number of drafts returned in this response. The endpoint returns all approvable drafts for the current user in one response — there is no pagination |
Each draft in the list contains:
| Field | Type | Description |
|---|---|---|
draftId | string | Unique draft identifier |
isValid | boolean | Whether the draft is ready for approval |
invalidReason | string | Localized reason why the draft is invalid (present when isValid is false) |
name | string | Descriptive name |
accountId | string | Associated account ID (if set) |
paymentError | string | Localized error from a failed payment attempt (present when a previous approval failed and the draft is re-approvable) |
Approve Drafts (Batch)
POST /payments/drafts/approveApproves and initiates execution of one or more drafts. A unique requestId is required to track the approval — it can be used to poll for status afterward.
| Field | Type | Required | Description |
|---|---|---|---|
requestId | string | Yes | Unique UUID for this approval request |
draftIds | string[] | Yes | Array of draft IDs to approve |
redirectUrl | string | Yes | URL the PSU is redirected to after completing Strong Customer Authentication |
The request is validated before any approval is initiated. If any draft ID
in draftIds does not exist, the entire request is rejected with a 404. No
drafts are approved in this case — ensure all draft IDs are valid before
submitting the batch.
Response:
| Field | Type | Description |
|---|---|---|
requestId | string | The request ID |
approvedDraftIds | string[] | Draft IDs that were actually approved |
status | string | Overall approval status (see Approval Status) |
authUrl | string | Authorization URL the PSU should be redirected to (present when the approval requires an SCA challenge) |
When authUrl is present, the TPP can redirect the PSU to it to complete Strong Customer Authentication. After the challenge, the PSU is redirected back to the redirectUrl supplied on the approve request (see Post-SCA Redirect).
| Status | Code | Description |
|---|---|---|
400 | MISSING_DRAFT_IDS | No draft IDs provided |
400 | MISSING_REDIRECT_URL | redirectUrl is missing from the request body |
400 | TRANSFER_DENIED | Approval was denied (e.g. account restrictions) |
404 | NOT_FOUND | One or more drafts not found — entire request rejected, no drafts approved |
409 | INVALID_STATE | None of the drafts are in an approvable state — e.g. already approved/executing, or not yet valid for approval (isValid: false) |
409 | ALREADY_USED | The requestId has already been used for a previous approval request |
The INVALID_STATE and ALREADY_USED errors are both returned as 409 Conflict but with different error codes. ALREADY_USED includes a field: "requestId" in the validation errors array to indicate the source of the
conflict.
Post-SCA Redirect
The batch approval flow follows the same authorization pattern as single-payment endpoints: the TPP redirects the PSU to an authorization URL, the PSU completes Strong Customer Authentication, and the PSU is then redirected back to a TPP-owned URL.
The redirectUrl is scoped to the approval request — each batch approve supplies its own single URL that the PSU is sent back to after completing SCA for that batch.
authUrl is only present in the response when an SCA challenge is required.
If approval completes without a challenge, no redirect is needed and the
authUrl field is omitted.
Get Approval Status
GET /payments/drafts/approve/{requestId}/statusReturns the current status of a batch approval request, including per-draft progress.
Response:
| Field | Type | Description |
|---|---|---|
status | string | Overall approval status (see Approval Status) |
drafts | array | Per-draft status details |
Each entry in drafts contains:
| Field | Type | Description |
|---|---|---|
draftId | string | Draft identifier |
status | string | One of CREATED, PENDING, COMPLETED, FAILED (see Per-Draft Status) |
errorMessage | string | Localized error message (present when status is FAILED; honors X-Language) |
paymentId | string | Resulting payment ID (present when status is PENDING or COMPLETED) |
expectedExecutionDate | string | Date the executing service expects to post the payment, in ISO 8601 YYYY-MM-DD form. Present when status is PENDING and an execution date was supplied; omitted otherwise. |
Errors:
| Status | Code | Description |
|---|---|---|
404 | NOT_FOUND | No approval request exists for the given requestId |
The paymentId is first surfaced when a draft reaches PENDING and remains
on the response through COMPLETED. It can be used to look up the payment via
its corresponding payment endpoint (e.g. GET /payments/se-bank-giro/ {paymentId}). The approval status response does not include the payment type
— your application must track which payment type each draft belongs to in
order to query the correct endpoint.
Per-Draft Status
| Status | Description |
|---|---|
CREATED | Draft is queued for approval — either the PSU has not yet completed SCA, or a previous payment attempt failed (errorMessage populated) and the draft is awaiting retry. |
PENDING | Payment has been accepted for execution by the underlying payment service. paymentId is set; expectedExecutionDate may be set when the payment is scheduled for a future date. |
COMPLETED | Payment has settled successfully. |
FAILED | Payment could not be completed; errorMessage describes the reason and the draft returns to CREATED for retry. |
Approval Status
The overall approval status reflects the state of the authorization challenge:
| Status | Description |
|---|---|
AWAITING_APPROVAL | Challenge has been created; waiting for user authorization |
APPROVED | All drafts have been approved and payments initiated |
EXPIRED | The authorization challenge expired before completion |
DECLINED | The user declined the authorization challenge |
Cross-layer Status Mapping
0.6.0-beta aligns the three status vocabularies that show up across a single signing-basket flow so a TPP only needs to learn one. All three layers use the same uppercase values, with controlled collapsing where the layers don’t have a 1:1 correspondence.
- Draft (
DraftResponse.status) — the lifecycle of an individual draft (GET /payments/drafts/{type}/{id}). Values:CREATED,PENDING,COMPLETED,DELETED. The draft stays inCREATEDwhile the basket approval is awaiting SCA and only transitions toPENDINGonce approval has succeeded —PENDINGtherefore covers awaiting clearing and awaiting settlement for future-dated payments, never awaiting SCA. - Per-draft basket (
DraftApprovalStatus.status) — the per-draft entry inside a basket approval response (GET /payments/drafts/approve/{requestId}/status). Values:CREATED,PENDING,COMPLETED,FAILED. Mirrors the draft layer, withFAILEDas a side path when the underlying payment couldn’t be completed (the draft itself recycles back toCREATEDso it can be retried). - Basket overall (
ApprovalStatus) — the overall approval/SCA challenge status returned by bothPOST /payments/drafts/approveand the status endpoint. Values:AWAITING_APPROVAL,APPROVED,EXPIRED,DECLINED. Models the SCA challenge itself, not the resulting payments. - Underlying payment (e.g.
SeBankGiroResponse.status) — the eventual payment’s lifecycle, looked up via the payment-type-specific endpoint. Values:AWAITING_APPROVAL,PENDING,APPROVED,COMPLETED,FAILED,DECLINED,CANCELLED. The fully detailed view — drafts only surface the post-approval lifecycle (the underlying payment’sPENDINGandCOMPLETED);AWAITING_APPROVALandAPPROVEDon the payment never appear at the draft layer because the draft is only handed off for execution after the basket has already been approved. Same-day vs future-dated transitions and explicit cancellation surface only on the payment endpoint.