Swift Signing SDK Integration Guide

Overview

The Swift signing SDK generates the following authentication data for Meitu Open Platform API requests:

  • X-Sdk-Date: the UTC request time.
  • Authorization: an HMAC-SHA256 signature generated from the AK/SK and request content.

The SDK returns a URLRequest containing the HTTP method, headers, and body, ready to send with URLSession.

Store the SK only on the backend; never embed it in an Apple client distributed to users.

Requirements

  • An Apple-platform Swift project.
  • The complete examples require Swift 5.5 or later and macOS 12 or later.
  • Foundation and CommonCrypto; SDK 1.0.0 does not use CryptoKit.
  • An accurate backend system clock; signature timestamps use UTC.

Download and integration

Download Swift SDK 1.0.0

The archive contains:

AIGCP-API-swift-sdk-1.0.0/
├── MTSigner.swift
└── MTDemo.swift

Add MTSigner.swift to the backend Swift target and confirm that the file belongs to the correct Target Membership. MTDemo.swift is a standalone demo entry point and is not required when integrating into an existing project.

API reference

The SDK exposes the following constants and methods:

public let kBasicDateFormat = "yyyyMMdd'T'HHmmss'Z'"
public let kAlgorithm = "SDK-HMAC-SHA256"
public let kHeaderXDate = "X-Sdk-Date"
public let kHeaderHost = "Host"
public let kHeaderAuthorization = "Authorization"
public let kHeaderContentSha256 = "X-Sdk-Content-Sha256"

public class MTSigner {
    public init(key: String, secret: String)

    public func signRequest(
        url: URL,
        method: String,
        headers: [String: String],
        body: String
    ) throws -> URLRequest
}
ParameterDescription
keyThe application's AK
secretThe SK paired with the AK
urlThe final request URL, including encoded and sorted query parameters
methodThe uppercase HTTP method required by the API, such as GET or POST
headersHeaders included in the signature; Host is required
bodyThe exact UTF-8 string to send; pass an empty string for GET
Return valueA URLRequest containing X-Sdk-Date, Authorization, and the request body

If headers already contains X-Sdk-Date, the SDK uses that value; otherwise it generates the UTC time automatically. Every input header is signed and must remain unchanged afterward.

Complete POST example

The following example calls the production synchronous task endpoint:

https://openapi.meitu.com/api/v1/sdk/sync/push

Set MEITU_OPENAPI_AK, MEITU_OPENAPI_SK, MEITU_TASK, and MEITU_IMAGE_URL before running it. Follow the target capability's API documentation for the values and structures of MEITU_TASK, images, params, task_type, and sync_timeout.

The request body contains exactly five top-level fields: task, task_type, init_images, params, and sync_timeout. params must be a JSON string.

import Foundation

enum IntegrationError: LocalizedError {
    case missingEnvironment(String)
    case invalidURL(String)
    case invalidUTF8Body
    case invalidHTTPResponse

    var errorDescription: String? {
        switch self {
        case .missingEnvironment(let name):
            return "Missing environment variable: \(name)"
        case .invalidURL(let value):
            return "Invalid URL: \(value)"
        case .invalidUTF8Body:
            return "Failed to create a stable UTF-8 request body"
        case .invalidHTTPResponse:
            return "The server did not return an HTTP response"
        }
    }
}

func requiredEnvironment(_ name: String) throws -> String {
    guard let value = ProcessInfo.processInfo.environment[name], !value.isEmpty else {
        throw IntegrationError.missingEnvironment(name)
    }
    return value
}

@main
struct PostExample {
    static func main() async {
        do {
            let accessKey = try requiredEnvironment("MEITU_OPENAPI_AK")
            let secretKey = try requiredEnvironment("MEITU_OPENAPI_SK")
            let taskName = try requiredEnvironment("MEITU_TASK")
            let imageURL = try requiredEnvironment("MEITU_IMAGE_URL")

            guard URL(string: imageURL) != nil else {
                throw IntegrationError.invalidURL(imageURL)
            }

            let endpoint = "https://openapi.meitu.com/api/v1/sdk/sync/push"
            guard let url = URL(string: endpoint) else {
                throw IntegrationError.invalidURL(endpoint)
            }

            let paramsObject: [String: Any] = [
                "parameter": ["rsp_media_type": "url"]
            ]
            let paramsData = try JSONSerialization.data(withJSONObject: paramsObject)
            guard let params = String(data: paramsData, encoding: .utf8) else {
                throw IntegrationError.invalidUTF8Body
            }

            let payload: [String: Any] = [
                "task": taskName,
                "task_type": "formula",
                "init_images": [[
                    "url": imageURL,
                    "profile": [
                        "media_profiles": ["media_data_type": "url"],
                        "version": "v1"
                    ]
                ]],
                "params": params,
                "sync_timeout": 30
            ]

            let bodyData = try JSONSerialization.data(withJSONObject: payload)
            guard let body = String(data: bodyData, encoding: .utf8),
                  body.data(using: .utf8) == bodyData else {
                throw IntegrationError.invalidUTF8Body
            }

            let headers: [String: String] = [
                kHeaderHost: "openapi.meitu.com",
                "Content-Type": "application/json; charset=utf-8",
                "Accept": "application/json"
            ]

            let signer = MTSigner(key: accessKey, secret: secretKey)
            let request = try signer.signRequest(
                url: url,
                method: "POST",
                headers: headers,
                body: body
            )

            guard request.httpBody == bodyData else {
                throw IntegrationError.invalidUTF8Body
            }

            let configuration = URLSessionConfiguration.ephemeral
            configuration.timeoutIntervalForRequest = 30
            configuration.timeoutIntervalForResource = 60
            let session = URLSession(configuration: configuration)
            let (responseData, response) = try await session.data(for: request)

            guard let httpResponse = response as? HTTPURLResponse else {
                throw IntegrationError.invalidHTTPResponse
            }

            let responseBody = String(data: responseData, encoding: .utf8) ?? ""
            print("HTTP \(httpResponse.statusCode)")
            print(responseBody)
        } catch {
            print("Request failed: \(error.localizedDescription)")
        }
    }
}

The code serializes the business payload once and sends the URLRequest returned by the signer directly. The same UTF-8 body Data is therefore used for signing and transmission.

GET example

GET requests use an empty body. Replace urlString with the final HTTPS URL of the target API. If it contains multiple query parameters, apply RFC 3986 encoding and sorting before signing.

import Foundation

enum GETExampleError: Error {
    case invalidURL
    case invalidHTTPResponse
}

func callSignedGET(
    accessKey: String,
    secretKey: String,
    urlString: String
) async throws -> (statusCode: Int, body: Data) {
    guard let url = URL(string: urlString),
          url.scheme == "https",
          url.host == "openapi.meitu.com" else {
        throw GETExampleError.invalidURL
    }

    let headers: [String: String] = [
        kHeaderHost: "openapi.meitu.com",
        "Accept": "application/json"
    ]

    let signer = MTSigner(key: accessKey, secret: secretKey)
    let request = try signer.signRequest(
        url: url,
        method: "GET",
        headers: headers,
        body: ""
    )

    let (data, response) = try await URLSession.shared.data(for: request)
    guard let httpResponse = response as? HTTPURLResponse else {
        throw GETExampleError.invalidHTTPResponse
    }
    return (httpResponse.statusCode, data)
}

Notes

  • Use HTTPS and set Host to openapi.meitu.com.
  • The method, URL, query, signed headers, and body must remain unchanged after signing. Send the request returned by signRequest directly.
  • Serialize JSON only once. Do not change fields, whitespace, line endings, or character encoding after signing.
  • Percent-encode and sort query parameters according to the signing specification before constructing the final URL.
  • X-Sdk-Date must be UTC yyyyMMdd'T'HHmmss'Z'; keep the backend clock synchronized.
  • By default, the SDK calculates SHA-256 over the UTF-8 body. Only when the target API explicitly supports it, add the following before signing:
var headers: [String: String] = [
    kHeaderHost: "openapi.meitu.com",
    "Content-Type": "application/json; charset=utf-8",
    kHeaderContentSha256: "UNSIGNED-PAYLOAD"
]
  • Keep the body of an AK/SK-authenticated request within 12 MB.

Common errors

SymptomResolution
no such module 'CommonCrypto'Use an Apple-platform Xcode/Swift toolchain and confirm that MTSigner.swift belongs to the correct target
401, 403, or signature verification failureCheck the AK/SK, backend time, HTTP method, Host, final URL, query, headers, and body for exact consistency
POST signature failureConfirm JSON is serialized only once and send the URLRequest returned by the SDK directly
GET signature failureConfirm query parameters are encoded and sorted and the URL is not changed after signing
UNSIGNED-PAYLOAD has no effectConfirm the target API supports it and use the exact header name and value
Request timeoutSet sync_timeout according to the target capability documentation and keep the URLSession timeout longer than the API processing time
Non-2xx responseRecord the redacted status code and response body, then handle the business error code according to the target capability API documentation